mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-04 13:55:23 -05:00
Merge #16528: Native Descriptor Wallets using DescriptorScriptPubKeyMan
223588b1bb
Add a --descriptors option to various tests (Andrew Chow)869f7ab30a
tests: Add RPCOverloadWrapper which overloads some disabled RPCs (Andrew Chow)cf06062859
Correctly check for default wallet (Andrew Chow)886e0d75f5
Implement CWallet::IsSpentKey for non-LegacySPKMans (Andrew Chow)3c19fdd2a2
Return error when no ScriptPubKeyMan is available for specified type (Andrew Chow)388ba94231
Change wallet_encryption.py to use signmessage instead of dumpprivkey (Andrew Chow)1346e14831
Functional tests for descriptor wallets (Andrew Chow)f193ea889d
add importdescriptors RPC and tests for native descriptor wallets (Hugo Nguyen)ce24a94494
Add IsLegacy to CWallet so that the GUI knows whether to show watchonly (Andrew Chow)1cb42b22b1
Generate new descriptors when encrypting (Andrew Chow)82ae02b165
Be able to create new wallets with DescriptorScriptPubKeyMans as backing (Andrew Chow)b713baa75a
Implement GetMetadata in DescriptorScriptPubKeyMan (Andrew Chow)8b9603bd0b
Change GetMetadata to use unique_ptr<CKeyMetadata> (Andrew Chow)72a9540df9
Implement FillPSBT in DescriptorScriptPubKeyMan (Andrew Chow)84b4978c02
Implement SignMessage for descriptor wallets (Andrew Chow)bde7c9fa38
Implement SignTransaction in DescriptorScriptPubKeyMan (Andrew Chow)d50c8ddd41
Implement GetSolvingProvider for DescriptorScriptPubKeyMan (Andrew Chow)f1ca5feb4a
Implement GetKeypoolOldestTime and only display it if greater than 0 (Andrew Chow)586b57a9a6
Implement ReturnDestination in DescriptorScriptPubKeyMan (Andrew Chow)f866957979
Implement GetReservedDestination in DescriptorScriptPubKeyMan (Andrew Chow)a775f7c7fd
Implement Unlock and Encrypt in DescriptorScriptPubKeyMan (Andrew Chow)bfdd073486
Implement GetNewDestination for DescriptorScriptPubKeyMan (Andrew Chow)58c7651821
Implement TopUp in DescriptorScriptPubKeyMan (Andrew Chow)e014886a34
Implement SetupGeneration for DescriptorScriptPubKeyMan (Andrew Chow)46dfb99768
Implement writing descriptorkeys, descriptorckeys, and descriptors to wallet file (Andrew Chow)4cb9b69be0
Implement several simple functions in DescriptorScriptPubKeyMan (Andrew Chow)d1ec3e4f19
Add IsSingleType to Descriptors (Andrew Chow)953feb3d27
Implement loading of keys for DescriptorScriptPubKeyMan (Andrew Chow)2363e9fcaa
Load the descriptor cache from the wallet file (Andrew Chow)46c46aebb7
Implement GetID for DescriptorScriptPubKeyMan (Andrew Chow)ec2f9e1178
Implement IsHDEnabled in DescriptorScriptPubKeyMan (Andrew Chow)741122d4c1
Implement MarkUnusedAddresses in DescriptorScriptPubKeyMan (Andrew Chow)2db7ca765c
Implement IsMine for DescriptorScriptPubKeyMan (Andrew Chow)db7177af8c
Add LoadDescriptorScriptPubKeyMan and SetActiveScriptPubKeyMan to CWallet (Andrew Chow)78f8a92910
Implement SetType in DescriptorScriptPubKeyMan (Andrew Chow)834de0300c
Store WalletDescriptor in DescriptorScriptPubKeyMan (Andrew Chow)d8132669e1
Add a lock cs_desc_man for DescriptorScriptPubKeyMan (Andrew Chow)3194a7f88a
Introduce WalletDescriptor class (Andrew Chow)6b13cd3fa8
Create LegacyScriptPubKeyMan when not a descriptor wallet (Andrew Chow)aeac157c9d
Return nullptr from GetLegacyScriptPubKeyMan if descriptor wallet (Andrew Chow)96accc73f0
Add WALLET_FLAG_DESCRIPTORS (Andrew Chow)6b8119af53
Introduce DescriptorScriptPubKeyMan as a dummy class (Andrew Chow)06620302c7
Introduce SetType function to tell ScriptPubKeyMans the type and internal-ness of it (Andrew Chow) Pull request description: Introducing the wallet of the glorious future (again): native descriptor wallets. With native descriptor wallets, addresses are generated from descriptors. Instead of generating keys and deriving addresses from keys, addresses come from the scriptPubKeys produced by a descriptor. Native descriptor wallets will be optional for now and can only be created by using `createwallet`. Descriptor wallets will store descriptors, master keys from the descriptor, and descriptor cache entries. Keys are derived from descriptors on the fly. In order to allow choosing different address types, 6 descriptors are needed for normal use. There is a pair of primary and change descriptors for each of the 3 address types. With the default keypool size of 1000, each descriptor has 1000 scriptPubKeys and descriptor cache entries pregenerated. This has a side effect of making wallets large since 6000 pubkeys are written to the wallet by default, instead of the current 2000. scriptPubKeys are kept only in memory and are generated every time a descriptor is loaded. By default, we use the standard BIP 44, 49, 84 derivation paths with an external and internal derivation chain for each. Descriptors can also be imported with a new `importdescriptors` RPC. Native descriptor wallets use the `ScriptPubKeyMan` interface introduced in #16341 to add a `DescriptorScriptPubKeyMan`. This defines a different IsMine which uses the simpler model of "does this scriptPubKey exist in this wallet". Furthermore, `DescriptorScriptPubKeyMan` does not have watchonly, so with native descriptor wallets, it is not possible to have a wallet with both watchonly and non-watchonly things. Rather a wallet with `disable_private_keys` needs to be used for watchonly things. A `--descriptor` option was added to some tests (`wallet_basic.py`, `wallet_encryption.py`, `wallet_keypool.py`, `wallet_keypool_topup.py`, and `wallet_labels.py`) to allow for these tests to use descriptor wallets. Additionally, several RPCs are disabled for descriptor wallets (`importprivkey`, `importpubkey`, `importaddress`, `importmulti`, `addmultisigaddress`, `dumpprivkey`, `dumpwallet`, `importwallet`, and `sethdseed`). ACKs for top commit: Sjors: utACK223588b1bb
(rebased, nits addressed) jonatack: Code review re-ACK223588b1bb
. fjahr: re-ACK223588b1bb
instagibbs: light re-ACK223588b
meshcollider: Code review ACK223588b1bb
Tree-SHA512: 59bc52aeddbb769ed5f420d5d240d8137847ac821b588eb616b34461253510c1717d6a70bab8765631738747336ae06f45ba39603ccd17f483843e5ed9a90986
This commit is contained in:
commit
eef90c14ed
40 changed files with 2791 additions and 324 deletions
|
@ -464,6 +464,7 @@ public:
|
|||
{
|
||||
RemoveWallet(m_wallet);
|
||||
}
|
||||
bool isLegacy() override { return m_wallet->IsLegacy(); }
|
||||
std::unique_ptr<Handler> handleUnload(UnloadFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyUnload.connect(fn));
|
||||
|
|
|
@ -266,6 +266,9 @@ public:
|
|||
// Remove wallet.
|
||||
virtual void remove() = 0;
|
||||
|
||||
//! Return whether is a legacy wallet
|
||||
virtual bool isLegacy() = 0;
|
||||
|
||||
//! Register handler for unload message.
|
||||
using UnloadFn = std::function<void()>;
|
||||
virtual std::unique_ptr<Handler> handleUnload(UnloadFn fn) = 0;
|
||||
|
|
|
@ -219,6 +219,11 @@ struct CExtPubKey {
|
|||
a.pubkey == b.pubkey;
|
||||
}
|
||||
|
||||
friend bool operator!=(const CExtPubKey &a, const CExtPubKey &b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
void Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const;
|
||||
void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]);
|
||||
bool Derive(CExtPubKey& out, unsigned int nChild) const;
|
||||
|
|
|
@ -60,3 +60,8 @@ bool CreateWalletDialog::isMakeBlankWalletChecked() const
|
|||
{
|
||||
return ui->blank_wallet_checkbox->isChecked();
|
||||
}
|
||||
|
||||
bool CreateWalletDialog::isDescriptorWalletChecked() const
|
||||
{
|
||||
return ui->descriptor_checkbox->isChecked();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ public:
|
|||
bool isEncryptWalletChecked() const;
|
||||
bool isDisablePrivateKeysChecked() const;
|
||||
bool isMakeBlankWalletChecked() const;
|
||||
bool isDescriptorWalletChecked() const;
|
||||
|
||||
private:
|
||||
Ui::CreateWalletDialog *ui;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>364</width>
|
||||
<height>185</height>
|
||||
<height>213</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -17,7 +17,7 @@
|
|||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>140</y>
|
||||
<y>170</y>
|
||||
<width>341</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
|
@ -106,6 +106,22 @@
|
|||
<string>Make Blank Wallet</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QCheckBox" name="descriptor_checkbox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>140</y>
|
||||
<width>171</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Use descriptors for scriptPubKey management</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Descriptor Wallet</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>wallet_name_line_edit</tabstop>
|
||||
|
|
|
@ -161,20 +161,27 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances)
|
|||
{
|
||||
int unit = walletModel->getOptionsModel()->getDisplayUnit();
|
||||
m_balances = balances;
|
||||
if (walletModel->wallet().privateKeysDisabled()) {
|
||||
ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
if (walletModel->wallet().isLegacy()) {
|
||||
if (walletModel->wallet().privateKeysDisabled()) {
|
||||
ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
} else {
|
||||
ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
}
|
||||
} else {
|
||||
ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways));
|
||||
}
|
||||
// only show immature (newly mined) balance if it's non-zero, so as not to complicate things
|
||||
// for the non-mining users
|
||||
|
|
|
@ -157,17 +157,40 @@ void ReceiveCoinsDialog::on_receiveButton_clicked()
|
|||
}
|
||||
}
|
||||
address = model->getAddressTableModel()->addRow(AddressTableModel::Receive, label, "", address_type);
|
||||
SendCoinsRecipient info(address, label,
|
||||
ui->reqAmount->value(), ui->reqMessage->text());
|
||||
ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->setModel(model);
|
||||
dialog->setInfo(info);
|
||||
dialog->show();
|
||||
clear();
|
||||
|
||||
/* Store request for later reference */
|
||||
model->getRecentRequestsTableModel()->addNewRequest(info);
|
||||
switch(model->getAddressTableModel()->getEditStatus())
|
||||
{
|
||||
case AddressTableModel::EditStatus::OK: {
|
||||
// Success
|
||||
SendCoinsRecipient info(address, label,
|
||||
ui->reqAmount->value(), ui->reqMessage->text());
|
||||
ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->setModel(model);
|
||||
dialog->setInfo(info);
|
||||
dialog->show();
|
||||
|
||||
/* Store request for later reference */
|
||||
model->getRecentRequestsTableModel()->addNewRequest(info);
|
||||
break;
|
||||
}
|
||||
case AddressTableModel::EditStatus::WALLET_UNLOCK_FAILURE:
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
tr("Could not unlock wallet."),
|
||||
QMessageBox::Ok, QMessageBox::Ok);
|
||||
break;
|
||||
case AddressTableModel::EditStatus::KEY_GENERATION_FAILURE:
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
tr("Could not generate new %1 address").arg(QString::fromStdString(FormatOutputType(address_type))),
|
||||
QMessageBox::Ok, QMessageBox::Ok);
|
||||
break;
|
||||
// These aren't valid return values for our action
|
||||
case AddressTableModel::EditStatus::INVALID_ADDRESS:
|
||||
case AddressTableModel::EditStatus::DUPLICATE_ADDRESS:
|
||||
case AddressTableModel::EditStatus::NO_CHANGES:
|
||||
assert(false);
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
void ReceiveCoinsDialog::on_recentRequestsView_doubleClicked(const QModelIndex &index)
|
||||
|
|
|
@ -226,6 +226,9 @@ void CreateWalletActivity::createWallet()
|
|||
if (m_create_wallet_dialog->isMakeBlankWalletChecked()) {
|
||||
flags |= WALLET_FLAG_BLANK_WALLET;
|
||||
}
|
||||
if (m_create_wallet_dialog->isDescriptorWalletChecked()) {
|
||||
flags |= WALLET_FLAG_DESCRIPTORS;
|
||||
}
|
||||
|
||||
QTimer::singleShot(500, worker(), [this, name, flags] {
|
||||
WalletCreationStatus status;
|
||||
|
|
|
@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "importpubkey", 2, "rescan" },
|
||||
{ "importmulti", 0, "requests" },
|
||||
{ "importmulti", 1, "options" },
|
||||
{ "importdescriptors", 0, "requests" },
|
||||
{ "verifychain", 0, "checklevel" },
|
||||
{ "verifychain", 1, "nblocks" },
|
||||
{ "getblockstats", 0, "hash_or_height" },
|
||||
|
@ -170,6 +171,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "createwallet", 1, "disable_private_keys"},
|
||||
{ "createwallet", 2, "blank"},
|
||||
{ "createwallet", 4, "avoid_reuse"},
|
||||
{ "createwallet", 5, "descriptors"},
|
||||
{ "getnodeaddresses", 0, "count"},
|
||||
{ "stop", 0, "wait" },
|
||||
};
|
||||
|
|
|
@ -575,6 +575,7 @@ public:
|
|||
default: return nullopt;
|
||||
}
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed raw(H) descriptor. */
|
||||
|
@ -602,6 +603,7 @@ public:
|
|||
default: return nullopt;
|
||||
}
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed pk(P) descriptor. */
|
||||
|
@ -611,6 +613,7 @@ protected:
|
|||
std::vector<CScript> MakeScripts(const std::vector<CPubKey>& keys, const CScript*, FlatSigningProvider&) const override { return Vector(GetScriptForRawPubKey(keys[0])); }
|
||||
public:
|
||||
PKDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pk") {}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed pkh(P) descriptor. */
|
||||
|
@ -626,6 +629,7 @@ protected:
|
|||
public:
|
||||
PKHDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pkh") {}
|
||||
Optional<OutputType> GetOutputType() const override { return OutputType::LEGACY; }
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed wpkh(P) descriptor. */
|
||||
|
@ -641,6 +645,7 @@ protected:
|
|||
public:
|
||||
WPKHDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "wpkh") {}
|
||||
Optional<OutputType> GetOutputType() const override { return OutputType::BECH32; }
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed combo(P) descriptor. */
|
||||
|
@ -664,6 +669,7 @@ protected:
|
|||
}
|
||||
public:
|
||||
ComboDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "combo") {}
|
||||
bool IsSingleType() const final { return false; }
|
||||
};
|
||||
|
||||
/** A parsed multi(...) or sortedmulti(...) descriptor */
|
||||
|
@ -683,6 +689,7 @@ protected:
|
|||
}
|
||||
public:
|
||||
MultisigDescriptor(int threshold, std::vector<std::unique_ptr<PubkeyProvider>> providers, bool sorted = false) : DescriptorImpl(std::move(providers), {}, sorted ? "sortedmulti" : "multi"), m_threshold(threshold), m_sorted(sorted) {}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed sh(...) descriptor. */
|
||||
|
@ -699,6 +706,7 @@ public:
|
|||
if (m_subdescriptor_arg->GetOutputType() == OutputType::BECH32) return OutputType::P2SH_SEGWIT;
|
||||
return OutputType::LEGACY;
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed wsh(...) descriptor. */
|
||||
|
@ -709,6 +717,7 @@ protected:
|
|||
public:
|
||||
WSHDescriptor(std::unique_ptr<DescriptorImpl> desc) : DescriptorImpl({}, std::move(desc), "wsh") {}
|
||||
Optional<OutputType> GetOutputType() const override { return OutputType::BECH32; }
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -87,6 +87,9 @@ struct Descriptor {
|
|||
/** Convert the descriptor back to a string, undoing parsing. */
|
||||
virtual std::string ToString() const = 0;
|
||||
|
||||
/** Whether this descriptor will return one scriptPubKey or multiple (aka is or is not combo) */
|
||||
virtual bool IsSingleType() const = 0;
|
||||
|
||||
/** Convert the descriptor to a private string. This fails if the provided provider does not have the relevant private keys. */
|
||||
virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0;
|
||||
|
||||
|
|
|
@ -1458,3 +1458,297 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
static UniValue ProcessDescriptorImport(CWallet * const pwallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
|
||||
{
|
||||
UniValue warnings(UniValue::VARR);
|
||||
UniValue result(UniValue::VOBJ);
|
||||
|
||||
try {
|
||||
if (!data.exists("desc")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
|
||||
}
|
||||
|
||||
const std::string& descriptor = data["desc"].get_str();
|
||||
const bool active = data.exists("active") ? data["active"].get_bool() : false;
|
||||
const bool internal = data.exists("internal") ? data["internal"].get_bool() : false;
|
||||
const std::string& label = data.exists("label") ? data["label"].get_str() : "";
|
||||
|
||||
// Parse descriptor string
|
||||
FlatSigningProvider keys;
|
||||
std::string error;
|
||||
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
|
||||
if (!parsed_desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
|
||||
}
|
||||
|
||||
// Range check
|
||||
int64_t range_start = 0, range_end = 1, next_index = 0;
|
||||
if (!parsed_desc->IsRange() && data.exists("range")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
|
||||
} else if (parsed_desc->IsRange()) {
|
||||
if (data.exists("range")) {
|
||||
auto range = ParseDescriptorRange(data["range"]);
|
||||
range_start = range.first;
|
||||
range_end = range.second + 1; // Specified range end is inclusive, but we need range end as exclusive
|
||||
} else {
|
||||
warnings.push_back("Range not given, using default keypool range");
|
||||
range_start = 0;
|
||||
range_end = gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE);
|
||||
}
|
||||
next_index = range_start;
|
||||
|
||||
if (data.exists("next_index")) {
|
||||
next_index = data["next_index"].get_int64();
|
||||
// bound checks
|
||||
if (next_index < range_start || next_index >= range_end) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "next_index is out of range");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active descriptors must be ranged
|
||||
if (active && !parsed_desc->IsRange()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Active descriptors must be ranged");
|
||||
}
|
||||
|
||||
// Ranged descriptors should not have a label
|
||||
if (data.exists("range") && data.exists("label")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptors should not have a label");
|
||||
}
|
||||
|
||||
// Internal addresses should not have a label either
|
||||
if (internal && data.exists("label")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
|
||||
}
|
||||
|
||||
// Combo descriptor check
|
||||
if (active && !parsed_desc->IsSingleType()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Combo descriptors cannot be set to active");
|
||||
}
|
||||
|
||||
// If the wallet disabled private keys, abort if private keys exist
|
||||
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled");
|
||||
}
|
||||
|
||||
// Need to ExpandPrivate to check if private keys are available for all pubkeys
|
||||
FlatSigningProvider expand_keys;
|
||||
std::vector<CScript> scripts;
|
||||
parsed_desc->Expand(0, keys, scripts, expand_keys);
|
||||
parsed_desc->ExpandPrivate(0, keys, expand_keys);
|
||||
|
||||
// Check if all private keys are provided
|
||||
bool have_all_privkeys = !expand_keys.keys.empty();
|
||||
for (const auto& entry : expand_keys.origins) {
|
||||
const CKeyID& key_id = entry.first;
|
||||
CKey key;
|
||||
if (!expand_keys.GetKey(key_id, key)) {
|
||||
have_all_privkeys = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If private keys are enabled, check some things.
|
||||
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
if (keys.keys.empty()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled");
|
||||
}
|
||||
if (!have_all_privkeys) {
|
||||
warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
|
||||
}
|
||||
}
|
||||
|
||||
WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
|
||||
|
||||
// Check if the wallet already contains the descriptor
|
||||
auto existing_spk_manager = pwallet->GetDescriptorScriptPubKeyMan(w_desc);
|
||||
if (existing_spk_manager) {
|
||||
LOCK(existing_spk_manager->cs_desc_man);
|
||||
if (range_start > existing_spk_manager->GetWalletDescriptor().range_start) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMS, strprintf("range_start can only decrease; current range = [%d,%d]", existing_spk_manager->GetWalletDescriptor().range_start, existing_spk_manager->GetWalletDescriptor().range_end));
|
||||
}
|
||||
}
|
||||
|
||||
// Add descriptor to the wallet
|
||||
auto spk_manager = pwallet->AddWalletDescriptor(w_desc, keys, label);
|
||||
if (spk_manager == nullptr) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s'", descriptor));
|
||||
}
|
||||
|
||||
// Set descriptor as active if necessary
|
||||
if (active) {
|
||||
if (!w_desc.descriptor->GetOutputType()) {
|
||||
warnings.push_back("Unknown output type, cannot set descriptor to active.");
|
||||
} else {
|
||||
pwallet->SetActiveScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), internal);
|
||||
}
|
||||
}
|
||||
|
||||
result.pushKV("success", UniValue(true));
|
||||
} catch (const UniValue& e) {
|
||||
result.pushKV("success", UniValue(false));
|
||||
result.pushKV("error", e);
|
||||
} catch (...) {
|
||||
result.pushKV("success", UniValue(false));
|
||||
|
||||
result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, "Missing required fields"));
|
||||
}
|
||||
if (warnings.size()) result.pushKV("warnings", warnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
UniValue importdescriptors(const JSONRPCRequest& main_request) {
|
||||
// Acquire the wallet
|
||||
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(main_request);
|
||||
CWallet* const pwallet = wallet.get();
|
||||
if (!EnsureWalletIsAvailable(pwallet, main_request.fHelp)) {
|
||||
return NullUniValue;
|
||||
}
|
||||
|
||||
RPCHelpMan{"importdescriptors",
|
||||
"\nImport descriptors. This will trigger a rescan of the blockchain based on the earliest timestamp of all descriptors being imported. Requires a new wallet backup.\n"
|
||||
"\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n"
|
||||
"may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n",
|
||||
{
|
||||
{"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported",
|
||||
{
|
||||
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "Descriptor to import."},
|
||||
{"active", RPCArg::Type::BOOL, /* default */ "false", "Set this descriptor to be the active descriptor for the corresponding output type/externality"},
|
||||
{"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in the form [begin,end]) to import"},
|
||||
{"next_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If a ranged descriptor is set to active, this specifies the next index to generate addresses from"},
|
||||
{"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, "Time from which to start rescanning the blockchain for this descriptor, in " + UNIX_EPOCH_TIME + "\n"
|
||||
" Use the string \"now\" to substitute the current synced blockchain time.\n"
|
||||
" \"now\" can be specified to bypass scanning, for outputs which are known to never have been used, and\n"
|
||||
" 0 can be specified to scan the entire blockchain. Blocks up to 2 hours before the earliest timestamp\n"
|
||||
" of all descriptors being imported will be scanned.",
|
||||
/* oneline_description */ "", {"timestamp | \"now\"", "integer / string"}
|
||||
},
|
||||
{"internal", RPCArg::Type::BOOL, /* default */ "false", "Whether matching outputs should be treated as not incoming payments (e.g. change)"},
|
||||
{"label", RPCArg::Type::STR, /* default */ "''", "Label to assign to the address, only allowed with internal=false"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"\"requests\""},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result",
|
||||
{
|
||||
{RPCResult::Type::OBJ, "", "",
|
||||
{
|
||||
{RPCResult::Type::BOOL, "success", ""},
|
||||
{RPCResult::Type::ARR, "warnings", /* optional */ true, "",
|
||||
{
|
||||
{RPCResult::Type::STR, "", ""},
|
||||
}},
|
||||
{RPCResult::Type::OBJ, "error", /* optional */ true, "",
|
||||
{
|
||||
{RPCResult::Type::ELISION, "", "JSONRPC error"},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"internal\": true }, "
|
||||
"{ \"desc\": \"<my desccriptor 2>\", \"label\": \"example 2\", \"timestamp\": 1455191480 }]'") +
|
||||
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"active\": true, \"range\": [0,100], \"label\": \"<my bech32 wallet>\" }]'")
|
||||
},
|
||||
}.Check(main_request);
|
||||
|
||||
// Make sure wallet is a descriptor wallet
|
||||
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "importdescriptors is not available for non-descriptor wallets");
|
||||
}
|
||||
|
||||
RPCTypeCheck(main_request.params, {UniValue::VARR, UniValue::VOBJ});
|
||||
|
||||
WalletRescanReserver reserver(*pwallet);
|
||||
if (!reserver.reserve()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
|
||||
}
|
||||
|
||||
const UniValue& requests = main_request.params[0];
|
||||
const int64_t minimum_timestamp = 1;
|
||||
int64_t now = 0;
|
||||
int64_t lowest_timestamp = 0;
|
||||
bool rescan = false;
|
||||
UniValue response(UniValue::VARR);
|
||||
{
|
||||
auto locked_chain = pwallet->chain().lock();
|
||||
LOCK(pwallet->cs_wallet);
|
||||
EnsureWalletIsUnlocked(pwallet);
|
||||
|
||||
CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now)));
|
||||
|
||||
// Get all timestamps and extract the lowest timestamp
|
||||
for (const UniValue& request : requests.getValues()) {
|
||||
// This throws an error if "timestamp" doesn't exist
|
||||
const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp);
|
||||
const UniValue result = ProcessDescriptorImport(pwallet, request, timestamp);
|
||||
response.push_back(result);
|
||||
|
||||
if (lowest_timestamp > timestamp ) {
|
||||
lowest_timestamp = timestamp;
|
||||
}
|
||||
|
||||
// If we know the chain tip, and at least one request was successful then allow rescan
|
||||
if (!rescan && result["success"].get_bool()) {
|
||||
rescan = true;
|
||||
}
|
||||
}
|
||||
pwallet->ConnectScriptPubKeyManNotifiers();
|
||||
}
|
||||
|
||||
// Rescan the blockchain using the lowest timestamp
|
||||
if (rescan) {
|
||||
int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */);
|
||||
{
|
||||
auto locked_chain = pwallet->chain().lock();
|
||||
LOCK(pwallet->cs_wallet);
|
||||
pwallet->ReacceptWalletTransactions();
|
||||
}
|
||||
|
||||
if (pwallet->IsAbortingRescan()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
|
||||
}
|
||||
|
||||
if (scanned_time > lowest_timestamp) {
|
||||
std::vector<UniValue> results = response.getValues();
|
||||
response.clear();
|
||||
response.setArray();
|
||||
|
||||
// Compose the response
|
||||
for (unsigned int i = 0; i < requests.size(); ++i) {
|
||||
const UniValue& request = requests.getValues().at(i);
|
||||
|
||||
// If the descriptor timestamp is within the successfully scanned
|
||||
// range, or if the import result already has an error set, let
|
||||
// the result stand unmodified. Otherwise replace the result
|
||||
// with an error message.
|
||||
if (scanned_time <= GetImportTimestamp(request, now) || results.at(i).exists("error")) {
|
||||
response.push_back(results.at(i));
|
||||
} else {
|
||||
UniValue result = UniValue(UniValue::VOBJ);
|
||||
result.pushKV("success", UniValue(false));
|
||||
result.pushKV(
|
||||
"error",
|
||||
JSONRPCError(
|
||||
RPC_MISC_ERROR,
|
||||
strprintf("Rescan failed for descriptor with timestamp %d. There was an error reading a "
|
||||
"block from time %d, which is after or within %d seconds of key creation, and "
|
||||
"could contain transactions pertaining to the desc. As a result, transactions "
|
||||
"and coins using this desc may not appear in the wallet. This error could be "
|
||||
"caused by pruning or data corruption (see bitcoind log for details) and could "
|
||||
"be dealt with by downloading and rescanning the relevant blocks (see -reindex "
|
||||
"and -rescan options).",
|
||||
GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW)));
|
||||
response.push_back(std::move(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -1851,7 +1851,7 @@ static UniValue keypoolrefill(const JSONRPCRequest& request)
|
|||
},
|
||||
}.Check(request);
|
||||
|
||||
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
if (pwallet->IsLegacy() && pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
|
||||
}
|
||||
|
||||
|
@ -2439,7 +2439,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
|||
{RPCResult::Type::STR_AMOUNT, "unconfirmed_balance", "DEPRECATED. Identical to getbalances().mine.untrusted_pending"},
|
||||
{RPCResult::Type::STR_AMOUNT, "immature_balance", "DEPRECATED. Identical to getbalances().mine.immature"},
|
||||
{RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"},
|
||||
{RPCResult::Type::NUM_TIME, "keypoololdest", "the " + UNIX_EPOCH_TIME + " of the oldest pre-generated key in the key pool"},
|
||||
{RPCResult::Type::NUM_TIME, "keypoololdest", "the " + UNIX_EPOCH_TIME + " of the oldest pre-generated key in the key pool. Legacy wallets only."},
|
||||
{RPCResult::Type::NUM, "keypoolsize", "how many new keys are pre-generated (only counts external keys)"},
|
||||
{RPCResult::Type::NUM, "keypoolsize_hd_internal", "how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)"},
|
||||
{RPCResult::Type::NUM_TIME, "unlocked_until", "the " + UNIX_EPOCH_TIME + " until which the wallet is unlocked for transfers, or 0 if the wallet is locked"},
|
||||
|
@ -2452,6 +2452,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
|||
{RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"},
|
||||
{RPCResult::Type::NUM, "progress", "scanning progress percentage [0.0, 1.0]"},
|
||||
}},
|
||||
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
|
||||
}},
|
||||
},
|
||||
RPCExamples{
|
||||
|
@ -2471,13 +2472,16 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
|||
|
||||
size_t kpExternalSize = pwallet->KeypoolCountExternalKeys();
|
||||
const auto bal = pwallet->GetBalance();
|
||||
int64_t kp_oldest = pwallet->GetOldestKeyPoolTime();
|
||||
obj.pushKV("walletname", pwallet->GetName());
|
||||
obj.pushKV("walletversion", pwallet->GetVersion());
|
||||
obj.pushKV("balance", ValueFromAmount(bal.m_mine_trusted));
|
||||
obj.pushKV("unconfirmed_balance", ValueFromAmount(bal.m_mine_untrusted_pending));
|
||||
obj.pushKV("immature_balance", ValueFromAmount(bal.m_mine_immature));
|
||||
obj.pushKV("txcount", (int)pwallet->mapWallet.size());
|
||||
obj.pushKV("keypoololdest", pwallet->GetOldestKeyPoolTime());
|
||||
if (kp_oldest > 0) {
|
||||
obj.pushKV("keypoololdest", kp_oldest);
|
||||
}
|
||||
obj.pushKV("keypoolsize", (int64_t)kpExternalSize);
|
||||
|
||||
LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan();
|
||||
|
@ -2505,6 +2509,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
|||
} else {
|
||||
obj.pushKV("scanning", false);
|
||||
}
|
||||
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
@ -2701,6 +2706,7 @@ static UniValue createwallet(const JSONRPCRequest& request)
|
|||
{"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."},
|
||||
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."},
|
||||
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
|
||||
{"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
|
@ -2737,6 +2743,9 @@ static UniValue createwallet(const JSONRPCRequest& request)
|
|||
if (!request.params[4].isNull() && request.params[4].get_bool()) {
|
||||
flags |= WALLET_FLAG_AVOID_REUSE;
|
||||
}
|
||||
if (!request.params[5].isNull() && request.params[5].get_bool()) {
|
||||
flags |= WALLET_FLAG_DESCRIPTORS;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
std::shared_ptr<CWallet> wallet;
|
||||
|
@ -3815,7 +3824,7 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
|
|||
|
||||
ScriptPubKeyMan* spk_man = pwallet->GetScriptPubKeyMan(scriptPubKey);
|
||||
if (spk_man) {
|
||||
if (const CKeyMetadata* meta = spk_man->GetMetadata(dest)) {
|
||||
if (const std::unique_ptr<CKeyMetadata> meta = spk_man->GetMetadata(dest)) {
|
||||
ret.pushKV("timestamp", meta->nCreateTime);
|
||||
if (meta->has_key_origin) {
|
||||
ret.pushKV("hdkeypath", WriteHDKeypath(meta->key_origin.path));
|
||||
|
@ -4280,6 +4289,7 @@ UniValue importwallet(const JSONRPCRequest& request);
|
|||
UniValue importprunedfunds(const JSONRPCRequest& request);
|
||||
UniValue removeprunedfunds(const JSONRPCRequest& request);
|
||||
UniValue importmulti(const JSONRPCRequest& request);
|
||||
UniValue importdescriptors(const JSONRPCRequest& request);
|
||||
|
||||
void RegisterWalletRPCCommands(interfaces::Chain& chain, std::vector<std::unique_ptr<interfaces::Handler>>& handlers)
|
||||
{
|
||||
|
@ -4293,7 +4303,7 @@ static const CRPCCommand commands[] =
|
|||
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} },
|
||||
{ "wallet", "backupwallet", &backupwallet, {"destination"} },
|
||||
{ "wallet", "bumpfee", &bumpfee, {"txid", "options"} },
|
||||
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} },
|
||||
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors"} },
|
||||
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
|
||||
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
|
||||
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
|
||||
|
@ -4309,6 +4319,7 @@ static const CRPCCommand commands[] =
|
|||
{ "wallet", "getbalances", &getbalances, {} },
|
||||
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
|
||||
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
|
||||
{ "wallet", "importdescriptors", &importdescriptors, {"requests"} },
|
||||
{ "wallet", "importmulti", &importmulti, {"requests","options"} },
|
||||
{ "wallet", "importprivkey", &importprivkey, {"privkey","label","rescan"} },
|
||||
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
|
||||
|
|
|
@ -568,7 +568,7 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb
|
|||
return TransactionError::OK;
|
||||
}
|
||||
|
||||
const CKeyMetadata* LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const
|
||||
std::unique_ptr<CKeyMetadata> LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const
|
||||
{
|
||||
LOCK(cs_KeyStore);
|
||||
|
||||
|
@ -576,14 +576,14 @@ const CKeyMetadata* LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& des
|
|||
if (!key_id.IsNull()) {
|
||||
auto it = mapKeyMetadata.find(key_id);
|
||||
if (it != mapKeyMetadata.end()) {
|
||||
return &it->second;
|
||||
return MakeUnique<CKeyMetadata>(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
CScript scriptPubKey = GetScriptForDestination(dest);
|
||||
auto it = m_script_metadata.find(CScriptID(scriptPubKey));
|
||||
if (it != m_script_metadata.end()) {
|
||||
return &it->second;
|
||||
return MakeUnique<CKeyMetadata>(it->second);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
@ -1497,3 +1497,668 @@ std::set<CKeyID> LegacyScriptPubKeyMan::GetKeys() const
|
|||
}
|
||||
return set_address;
|
||||
}
|
||||
|
||||
void LegacyScriptPubKeyMan::SetType(OutputType type, bool internal) {}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error)
|
||||
{
|
||||
// Returns true if this descriptor supports getting new addresses. Conditions where we may be unable to fetch them (e.g. locked) are caught later
|
||||
if (!CanGetAddresses(m_internal)) {
|
||||
error = "No addresses available";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
assert(m_wallet_descriptor.descriptor->IsSingleType()); // This is a combo descriptor which should not be an active descriptor
|
||||
if (type != m_address_type) {
|
||||
throw std::runtime_error(std::string(__func__) + ": Types are inconsistent");
|
||||
}
|
||||
|
||||
TopUp();
|
||||
|
||||
// Get the scriptPubKey from the descriptor
|
||||
FlatSigningProvider out_keys;
|
||||
std::vector<CScript> scripts_temp;
|
||||
if (m_wallet_descriptor.range_end <= m_max_cached_index && !TopUp(1)) {
|
||||
// We can't generate anymore keys
|
||||
error = "Error: Keypool ran out, please call keypoolrefill first";
|
||||
return false;
|
||||
}
|
||||
if (!m_wallet_descriptor.descriptor->ExpandFromCache(m_wallet_descriptor.next_index, m_wallet_descriptor.cache, scripts_temp, out_keys)) {
|
||||
// We can't generate anymore keys
|
||||
error = "Error: Keypool ran out, please call keypoolrefill first";
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<OutputType> out_script_type = m_wallet_descriptor.descriptor->GetOutputType();
|
||||
if (out_script_type && out_script_type == type) {
|
||||
ExtractDestination(scripts_temp[0], dest);
|
||||
} else {
|
||||
throw std::runtime_error(std::string(__func__) + ": Types are inconsistent. Stored type does not match type of newly generated address");
|
||||
}
|
||||
m_wallet_descriptor.next_index++;
|
||||
WalletBatch(m_storage.GetDatabase()).WriteDescriptor(GetID(), m_wallet_descriptor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
if (m_map_script_pub_keys.count(script) > 0) {
|
||||
return ISMINE_SPENDABLE;
|
||||
}
|
||||
return ISMINE_NO;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
if (!m_map_keys.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool keyPass = m_map_crypted_keys.empty(); // Always pass when there are no encrypted keys
|
||||
bool keyFail = false;
|
||||
for (const auto& mi : m_map_crypted_keys) {
|
||||
const CPubKey &pubkey = mi.second.first;
|
||||
const std::vector<unsigned char> &crypted_secret = mi.second.second;
|
||||
CKey key;
|
||||
if (!DecryptKey(master_key, crypted_secret, pubkey, key)) {
|
||||
keyFail = true;
|
||||
break;
|
||||
}
|
||||
keyPass = true;
|
||||
if (m_decryption_thoroughly_checked)
|
||||
break;
|
||||
}
|
||||
if (keyPass && keyFail) {
|
||||
LogPrintf("The wallet is probably corrupted: Some keys decrypt but not all.\n");
|
||||
throw std::runtime_error("Error unlocking wallet: some keys decrypt but not all. Your wallet file may be corrupt.");
|
||||
}
|
||||
if (keyFail || (!keyPass && !accept_no_keys)) {
|
||||
return false;
|
||||
}
|
||||
m_decryption_thoroughly_checked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
if (!m_map_crypted_keys.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const KeyMap::value_type& key_in : m_map_keys)
|
||||
{
|
||||
const CKey &key = key_in.second;
|
||||
CPubKey pubkey = key.GetPubKey();
|
||||
CKeyingMaterial secret(key.begin(), key.end());
|
||||
std::vector<unsigned char> crypted_secret;
|
||||
if (!EncryptSecret(master_key, secret, pubkey.GetHash(), crypted_secret)) {
|
||||
return false;
|
||||
}
|
||||
m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret);
|
||||
batch->WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret);
|
||||
}
|
||||
m_map_keys.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
std::string error;
|
||||
bool result = GetNewDestination(type, address, error);
|
||||
index = m_wallet_descriptor.next_index - 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::ReturnDestination(int64_t index, bool internal, const CTxDestination& addr)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
// Only return when the index was the most recent
|
||||
if (m_wallet_descriptor.next_index - 1 == index) {
|
||||
m_wallet_descriptor.next_index--;
|
||||
}
|
||||
WalletBatch(m_storage.GetDatabase()).WriteDescriptor(GetID(), m_wallet_descriptor);
|
||||
NotifyCanGetAddressesChanged();
|
||||
}
|
||||
|
||||
std::map<CKeyID, CKey> DescriptorScriptPubKeyMan::GetKeys() const
|
||||
{
|
||||
AssertLockHeld(cs_desc_man);
|
||||
if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked()) {
|
||||
KeyMap keys;
|
||||
for (auto key_pair : m_map_crypted_keys) {
|
||||
const CPubKey& pubkey = key_pair.second.first;
|
||||
const std::vector<unsigned char>& crypted_secret = key_pair.second.second;
|
||||
CKey key;
|
||||
DecryptKey(m_storage.GetEncryptionKey(), crypted_secret, pubkey, key);
|
||||
keys[pubkey.GetID()] = key;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
return m_map_keys;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::TopUp(unsigned int size)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
unsigned int target_size;
|
||||
if (size > 0) {
|
||||
target_size = size;
|
||||
} else {
|
||||
target_size = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 1);
|
||||
}
|
||||
|
||||
// Calculate the new range_end
|
||||
int32_t new_range_end = std::max(m_wallet_descriptor.next_index + (int32_t)target_size, m_wallet_descriptor.range_end);
|
||||
|
||||
// If the descriptor is not ranged, we actually just want to fill the first cache item
|
||||
if (!m_wallet_descriptor.descriptor->IsRange()) {
|
||||
new_range_end = 1;
|
||||
m_wallet_descriptor.range_end = 1;
|
||||
m_wallet_descriptor.range_start = 0;
|
||||
}
|
||||
|
||||
FlatSigningProvider provider;
|
||||
provider.keys = GetKeys();
|
||||
|
||||
WalletBatch batch(m_storage.GetDatabase());
|
||||
uint256 id = GetID();
|
||||
for (int32_t i = m_max_cached_index + 1; i < new_range_end; ++i) {
|
||||
FlatSigningProvider out_keys;
|
||||
std::vector<CScript> scripts_temp;
|
||||
DescriptorCache temp_cache;
|
||||
// Maybe we have a cached xpub and we can expand from the cache first
|
||||
if (!m_wallet_descriptor.descriptor->ExpandFromCache(i, m_wallet_descriptor.cache, scripts_temp, out_keys)) {
|
||||
if (!m_wallet_descriptor.descriptor->Expand(i, provider, scripts_temp, out_keys, &temp_cache)) return false;
|
||||
}
|
||||
// Add all of the scriptPubKeys to the scriptPubKey set
|
||||
for (const CScript& script : scripts_temp) {
|
||||
m_map_script_pub_keys[script] = i;
|
||||
}
|
||||
for (const auto& pk_pair : out_keys.pubkeys) {
|
||||
const CPubKey& pubkey = pk_pair.second;
|
||||
if (m_map_pubkeys.count(pubkey) != 0) {
|
||||
// We don't need to give an error here.
|
||||
// It doesn't matter which of many valid indexes the pubkey has, we just need an index where we can derive it and it's private key
|
||||
continue;
|
||||
}
|
||||
m_map_pubkeys[pubkey] = i;
|
||||
}
|
||||
// Write the cache
|
||||
for (const auto& parent_xpub_pair : temp_cache.GetCachedParentExtPubKeys()) {
|
||||
CExtPubKey xpub;
|
||||
if (m_wallet_descriptor.cache.GetCachedParentExtPubKey(parent_xpub_pair.first, xpub)) {
|
||||
if (xpub != parent_xpub_pair.second) {
|
||||
throw std::runtime_error(std::string(__func__) + ": New cached parent xpub does not match already cached parent xpub");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!batch.WriteDescriptorParentCache(parent_xpub_pair.second, id, parent_xpub_pair.first)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
|
||||
}
|
||||
m_wallet_descriptor.cache.CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second);
|
||||
}
|
||||
for (const auto& derived_xpub_map_pair : temp_cache.GetCachedDerivedExtPubKeys()) {
|
||||
for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) {
|
||||
CExtPubKey xpub;
|
||||
if (m_wallet_descriptor.cache.GetCachedDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, xpub)) {
|
||||
if (xpub != derived_xpub_pair.second) {
|
||||
throw std::runtime_error(std::string(__func__) + ": New cached derived xpub does not match already cached derived xpub");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!batch.WriteDescriptorDerivedCache(derived_xpub_pair.second, id, derived_xpub_map_pair.first, derived_xpub_pair.first)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
|
||||
}
|
||||
m_wallet_descriptor.cache.CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second);
|
||||
}
|
||||
}
|
||||
m_max_cached_index++;
|
||||
}
|
||||
m_wallet_descriptor.range_end = new_range_end;
|
||||
batch.WriteDescriptor(GetID(), m_wallet_descriptor);
|
||||
|
||||
// By this point, the cache size should be the size of the entire range
|
||||
assert(m_wallet_descriptor.range_end - 1 == m_max_cached_index);
|
||||
|
||||
NotifyCanGetAddressesChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
if (IsMine(script)) {
|
||||
int32_t index = m_map_script_pub_keys[script];
|
||||
if (index >= m_wallet_descriptor.next_index) {
|
||||
WalletLogPrintf("%s: Detected a used keypool item at index %d, mark all keypool items up to this item as used\n", __func__, index);
|
||||
m_wallet_descriptor.next_index = index + 1;
|
||||
}
|
||||
if (!TopUp()) {
|
||||
WalletLogPrintf("%s: Topping up keypool failed (locked wallet)\n", __func__);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey &pubkey)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
WalletBatch batch(m_storage.GetDatabase());
|
||||
if (!AddDescriptorKeyWithDB(batch, key, pubkey)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed");
|
||||
}
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey)
|
||||
{
|
||||
AssertLockHeld(cs_desc_man);
|
||||
assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
|
||||
|
||||
if (m_storage.HasEncryptionKeys()) {
|
||||
if (m_storage.IsLocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> crypted_secret;
|
||||
CKeyingMaterial secret(key.begin(), key.end());
|
||||
if (!EncryptSecret(m_storage.GetEncryptionKey(), secret, pubkey.GetHash(), crypted_secret)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret);
|
||||
return batch.WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret);
|
||||
} else {
|
||||
m_map_keys[pubkey.GetID()] = key;
|
||||
return batch.WriteDescriptorKey(GetID(), pubkey, key.GetPrivKey());
|
||||
}
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
||||
|
||||
// Ignore when there is already a descriptor
|
||||
if (m_wallet_descriptor.descriptor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t creation_time = GetTime();
|
||||
|
||||
std::string xpub = EncodeExtPubKey(master_key.Neuter());
|
||||
|
||||
// Build descriptor string
|
||||
std::string desc_prefix;
|
||||
std::string desc_suffix = "/*)";
|
||||
switch (m_address_type) {
|
||||
case OutputType::LEGACY: {
|
||||
desc_prefix = "pkh(" + xpub + "/44'";
|
||||
break;
|
||||
}
|
||||
case OutputType::P2SH_SEGWIT: {
|
||||
desc_prefix = "sh(wpkh(" + xpub + "/49'";
|
||||
desc_suffix += ")";
|
||||
break;
|
||||
}
|
||||
case OutputType::BECH32: {
|
||||
desc_prefix = "wpkh(" + xpub + "/84'";
|
||||
break;
|
||||
}
|
||||
default: assert(false);
|
||||
}
|
||||
|
||||
// Mainnet derives at 0', testnet and regtest derive at 1'
|
||||
if (Params().IsTestChain()) {
|
||||
desc_prefix += "/1'";
|
||||
} else {
|
||||
desc_prefix += "/0'";
|
||||
}
|
||||
|
||||
std::string internal_path = m_internal ? "/1" : "/0";
|
||||
std::string desc_str = desc_prefix + "/0'" + internal_path + desc_suffix;
|
||||
|
||||
// Make the descriptor
|
||||
FlatSigningProvider keys;
|
||||
std::string error;
|
||||
std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, error, false);
|
||||
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
|
||||
m_wallet_descriptor = w_desc;
|
||||
|
||||
// Store the master private key, and descriptor
|
||||
WalletBatch batch(m_storage.GetDatabase());
|
||||
if (!AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey())) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing descriptor master private key failed");
|
||||
}
|
||||
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
|
||||
}
|
||||
|
||||
// TopUp
|
||||
TopUp();
|
||||
|
||||
m_storage.UnsetBlankWalletFlag(batch);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::IsHDEnabled() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
return m_wallet_descriptor.descriptor->IsRange();
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::CanGetAddresses(bool internal) const
|
||||
{
|
||||
// We can only give out addresses from descriptors that are single type (not combo), ranged,
|
||||
// and either have cached keys or can generate more keys (ignoring encryption)
|
||||
LOCK(cs_desc_man);
|
||||
return m_wallet_descriptor.descriptor->IsSingleType() &&
|
||||
m_wallet_descriptor.descriptor->IsRange() &&
|
||||
(HavePrivateKeys() || m_wallet_descriptor.next_index < m_wallet_descriptor.range_end);
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::HavePrivateKeys() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
return m_map_keys.size() > 0 || m_map_crypted_keys.size() > 0;
|
||||
}
|
||||
|
||||
int64_t DescriptorScriptPubKeyMan::GetOldestKeyPoolTime() const
|
||||
{
|
||||
// This is only used for getwalletinfo output and isn't relevant to descriptor wallets.
|
||||
// The magic number 0 indicates that it shouldn't be displayed so that's what we return.
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t DescriptorScriptPubKeyMan::KeypoolCountExternalKeys() const
|
||||
{
|
||||
if (m_internal) {
|
||||
return 0;
|
||||
}
|
||||
return GetKeyPoolSize();
|
||||
}
|
||||
|
||||
unsigned int DescriptorScriptPubKeyMan::GetKeyPoolSize() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
return m_wallet_descriptor.range_end - m_wallet_descriptor.next_index;
|
||||
}
|
||||
|
||||
int64_t DescriptorScriptPubKeyMan::GetTimeFirstKey() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
return m_wallet_descriptor.creation_time;
|
||||
}
|
||||
|
||||
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(const CScript& script, bool include_private) const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
|
||||
// Find the index of the script
|
||||
auto it = m_map_script_pub_keys.find(script);
|
||||
if (it == m_map_script_pub_keys.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
int32_t index = it->second;
|
||||
|
||||
return GetSigningProvider(index, include_private);
|
||||
}
|
||||
|
||||
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(const CPubKey& pubkey) const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
|
||||
// Find index of the pubkey
|
||||
auto it = m_map_pubkeys.find(pubkey);
|
||||
if (it == m_map_pubkeys.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
int32_t index = it->second;
|
||||
|
||||
// Always try to get the signing provider with private keys. This function should only be called during signing anyways
|
||||
return GetSigningProvider(index, true);
|
||||
}
|
||||
|
||||
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const
|
||||
{
|
||||
AssertLockHeld(cs_desc_man);
|
||||
// Get the scripts, keys, and key origins for this script
|
||||
std::unique_ptr<FlatSigningProvider> out_keys = MakeUnique<FlatSigningProvider>();
|
||||
std::vector<CScript> scripts_temp;
|
||||
if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr;
|
||||
|
||||
if (HavePrivateKeys() && include_private) {
|
||||
FlatSigningProvider master_provider;
|
||||
master_provider.keys = GetKeys();
|
||||
m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
|
||||
}
|
||||
|
||||
return out_keys;
|
||||
}
|
||||
|
||||
std::unique_ptr<SigningProvider> DescriptorScriptPubKeyMan::GetSolvingProvider(const CScript& script) const
|
||||
{
|
||||
return GetSigningProvider(script, false);
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sigdata)
|
||||
{
|
||||
return IsMine(script);
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const
|
||||
{
|
||||
std::unique_ptr<FlatSigningProvider> keys = MakeUnique<FlatSigningProvider>();
|
||||
for (const auto& coin_pair : coins) {
|
||||
std::unique_ptr<FlatSigningProvider> coin_keys = GetSigningProvider(coin_pair.second.out.scriptPubKey, true);
|
||||
if (!coin_keys) {
|
||||
continue;
|
||||
}
|
||||
*keys = Merge(*keys, *coin_keys);
|
||||
}
|
||||
|
||||
return ::SignTransaction(tx, keys.get(), coins, sighash, input_errors);
|
||||
}
|
||||
|
||||
SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const
|
||||
{
|
||||
std::unique_ptr<FlatSigningProvider> keys = GetSigningProvider(GetScriptForDestination(pkhash), true);
|
||||
if (!keys) {
|
||||
return SigningResult::PRIVATE_KEY_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
CKeyID key_id(pkhash);
|
||||
CKey key;
|
||||
if (!keys->GetKey(key_id, key)) {
|
||||
return SigningResult::PRIVATE_KEY_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
if (!MessageSign(key, message, str_sig)) {
|
||||
return SigningResult::SIGNING_FAILED;
|
||||
}
|
||||
return SigningResult::OK;
|
||||
}
|
||||
|
||||
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const
|
||||
{
|
||||
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
|
||||
const CTxIn& txin = psbtx.tx->vin[i];
|
||||
PSBTInput& input = psbtx.inputs.at(i);
|
||||
|
||||
if (PSBTInputSigned(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify input looks sane. This will check that we have at most one uxto, witness or non-witness.
|
||||
if (!input.IsSane()) {
|
||||
return TransactionError::INVALID_PSBT;
|
||||
}
|
||||
|
||||
// Get the Sighash type
|
||||
if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) {
|
||||
return TransactionError::SIGHASH_MISMATCH;
|
||||
}
|
||||
|
||||
// Get the scriptPubKey to know which SigningProvider to use
|
||||
CScript script;
|
||||
if (!input.witness_utxo.IsNull()) {
|
||||
script = input.witness_utxo.scriptPubKey;
|
||||
} else if (input.non_witness_utxo) {
|
||||
if (txin.prevout.n >= input.non_witness_utxo->vout.size()) {
|
||||
return TransactionError::MISSING_INPUTS;
|
||||
}
|
||||
script = input.non_witness_utxo->vout[txin.prevout.n].scriptPubKey;
|
||||
} else {
|
||||
// There's no UTXO so we can just skip this now
|
||||
continue;
|
||||
}
|
||||
SignatureData sigdata;
|
||||
input.FillSignatureData(sigdata);
|
||||
|
||||
std::unique_ptr<FlatSigningProvider> keys = MakeUnique<FlatSigningProvider>();
|
||||
std::unique_ptr<FlatSigningProvider> script_keys = GetSigningProvider(script, sign);
|
||||
if (script_keys) {
|
||||
*keys = Merge(*keys, *script_keys);
|
||||
} else {
|
||||
// Maybe there are pubkeys listed that we can sign for
|
||||
script_keys = MakeUnique<FlatSigningProvider>();
|
||||
for (const auto& pk_pair : input.hd_keypaths) {
|
||||
const CPubKey& pubkey = pk_pair.first;
|
||||
std::unique_ptr<FlatSigningProvider> pk_keys = GetSigningProvider(pubkey);
|
||||
if (pk_keys) {
|
||||
*keys = Merge(*keys, *pk_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, sighash_type);
|
||||
}
|
||||
|
||||
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
|
||||
for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) {
|
||||
std::unique_ptr<SigningProvider> keys = GetSolvingProvider(psbtx.tx->vout.at(i).scriptPubKey);
|
||||
if (!keys) {
|
||||
continue;
|
||||
}
|
||||
UpdatePSBTOutput(HidingSigningProvider(keys.get(), true, !bip32derivs), psbtx, i);
|
||||
}
|
||||
|
||||
return TransactionError::OK;
|
||||
}
|
||||
|
||||
std::unique_ptr<CKeyMetadata> DescriptorScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const
|
||||
{
|
||||
std::unique_ptr<SigningProvider> provider = GetSigningProvider(GetScriptForDestination(dest));
|
||||
if (provider) {
|
||||
KeyOriginInfo orig;
|
||||
CKeyID key_id = GetKeyForDestination(*provider, dest);
|
||||
if (provider->GetKeyOrigin(key_id, orig)) {
|
||||
LOCK(cs_desc_man);
|
||||
std::unique_ptr<CKeyMetadata> meta = MakeUnique<CKeyMetadata>();
|
||||
meta->key_origin = orig;
|
||||
meta->has_key_origin = true;
|
||||
meta->nCreateTime = m_wallet_descriptor.creation_time;
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint256 DescriptorScriptPubKeyMan::GetID() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
std::string desc_str = m_wallet_descriptor.descriptor->ToString();
|
||||
uint256 id;
|
||||
CSHA256().Write((unsigned char*)desc_str.data(), desc_str.size()).Finalize(id.begin());
|
||||
return id;
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::SetType(OutputType type, bool internal)
|
||||
{
|
||||
this->m_address_type = type;
|
||||
this->m_internal = internal;
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
m_wallet_descriptor.cache = cache;
|
||||
for (int32_t i = m_wallet_descriptor.range_start; i < m_wallet_descriptor.range_end; ++i) {
|
||||
FlatSigningProvider out_keys;
|
||||
std::vector<CScript> scripts_temp;
|
||||
if (!m_wallet_descriptor.descriptor->ExpandFromCache(i, m_wallet_descriptor.cache, scripts_temp, out_keys)) {
|
||||
throw std::runtime_error("Error: Unable to expand wallet descriptor from cache");
|
||||
}
|
||||
// Add all of the scriptPubKeys to the scriptPubKey set
|
||||
for (const CScript& script : scripts_temp) {
|
||||
if (m_map_script_pub_keys.count(script) != 0) {
|
||||
throw std::runtime_error(strprintf("Error: Already loaded script at index %d as being at index %d", i, m_map_script_pub_keys[script]));
|
||||
}
|
||||
m_map_script_pub_keys[script] = i;
|
||||
}
|
||||
for (const auto& pk_pair : out_keys.pubkeys) {
|
||||
const CPubKey& pubkey = pk_pair.second;
|
||||
if (m_map_pubkeys.count(pubkey) != 0) {
|
||||
// We don't need to give an error here.
|
||||
// It doesn't matter which of many valid indexes the pubkey has, we just need an index where we can derive it and it's private key
|
||||
continue;
|
||||
}
|
||||
m_map_pubkeys[pubkey] = i;
|
||||
}
|
||||
m_max_cached_index++;
|
||||
}
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::AddKey(const CKeyID& key_id, const CKey& key)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
m_map_keys[key_id] = key;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
if (!m_map_keys.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
return m_wallet_descriptor.descriptor != nullptr && desc.descriptor != nullptr && m_wallet_descriptor.descriptor->ToString() == desc.descriptor->ToString();
|
||||
}
|
||||
|
||||
void DescriptorScriptPubKeyMan::WriteDescriptor()
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
WalletBatch batch(m_storage.GetDatabase());
|
||||
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
|
||||
}
|
||||
}
|
||||
|
||||
const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const
|
||||
{
|
||||
return m_wallet_descriptor;
|
||||
}
|
||||
|
||||
const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
std::vector<CScript> script_pub_keys;
|
||||
script_pub_keys.reserve(m_map_script_pub_keys.size());
|
||||
|
||||
for (auto const& script_pub_key: m_map_script_pub_keys) {
|
||||
script_pub_keys.push_back(script_pub_key.first);
|
||||
}
|
||||
return script_pub_keys;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#define BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
|
||||
|
||||
#include <psbt.h>
|
||||
#include <script/descriptor.h>
|
||||
#include <script/signingprovider.h>
|
||||
#include <script/standard.h>
|
||||
#include <util/error.h>
|
||||
|
@ -204,7 +205,7 @@ public:
|
|||
|
||||
virtual int64_t GetTimeFirstKey() const { return 0; }
|
||||
|
||||
virtual const CKeyMetadata* GetMetadata(const CTxDestination& dest) const { return nullptr; }
|
||||
virtual std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const { return nullptr; }
|
||||
|
||||
virtual std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const { return nullptr; }
|
||||
|
||||
|
@ -222,6 +223,8 @@ public:
|
|||
|
||||
virtual uint256 GetID() const { return uint256(); }
|
||||
|
||||
virtual void SetType(OutputType type, bool internal) {}
|
||||
|
||||
/** Prepends the wallet name in logging output to ease debugging in multi-wallet use cases */
|
||||
template<typename... Params>
|
||||
void WalletLogPrintf(std::string fmt, Params... parameters) const {
|
||||
|
@ -352,7 +355,7 @@ public:
|
|||
|
||||
int64_t GetTimeFirstKey() const override;
|
||||
|
||||
const CKeyMetadata* GetMetadata(const CTxDestination& dest) const override;
|
||||
std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const override;
|
||||
|
||||
bool CanGetAddresses(bool internal = false) const override;
|
||||
|
||||
|
@ -366,6 +369,8 @@ public:
|
|||
|
||||
uint256 GetID() const override;
|
||||
|
||||
void SetType(OutputType type, bool internal) override;
|
||||
|
||||
// Map from Key ID to key metadata.
|
||||
std::map<CKeyID, CKeyMetadata> mapKeyMetadata GUARDED_BY(cs_KeyStore);
|
||||
|
||||
|
@ -477,4 +482,111 @@ public:
|
|||
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override { return m_spk_man.GetKeyOrigin(keyid, info); }
|
||||
};
|
||||
|
||||
class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
|
||||
{
|
||||
private:
|
||||
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
|
||||
|
||||
using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
|
||||
using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
|
||||
using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
|
||||
using KeyMap = std::map<CKeyID, CKey>;
|
||||
|
||||
ScriptPubKeyMap m_map_script_pub_keys GUARDED_BY(cs_desc_man);
|
||||
PubKeyMap m_map_pubkeys GUARDED_BY(cs_desc_man);
|
||||
int32_t m_max_cached_index = -1;
|
||||
|
||||
OutputType m_address_type;
|
||||
bool m_internal;
|
||||
|
||||
KeyMap m_map_keys GUARDED_BY(cs_desc_man);
|
||||
CryptedKeyMap m_map_crypted_keys GUARDED_BY(cs_desc_man);
|
||||
|
||||
bool SetCrypted();
|
||||
|
||||
//! keeps track of whether Unlock has run a thorough check before
|
||||
bool m_decryption_thoroughly_checked = false;
|
||||
|
||||
bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey);
|
||||
|
||||
KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
// Fetch the SigningProvider for the given script and optionally include private keys
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(const CScript& script, bool include_private = false) const;
|
||||
// Fetch the SigningProvider for the given pubkey and always include private keys. This should only be called by signing code.
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(const CPubKey& pubkey) const;
|
||||
// Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
public:
|
||||
DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor)
|
||||
: ScriptPubKeyMan(storage),
|
||||
m_wallet_descriptor(descriptor)
|
||||
{}
|
||||
DescriptorScriptPubKeyMan(WalletStorage& storage, OutputType address_type, bool internal)
|
||||
: ScriptPubKeyMan(storage),
|
||||
m_address_type(address_type), m_internal(internal)
|
||||
{}
|
||||
|
||||
mutable RecursiveMutex cs_desc_man;
|
||||
|
||||
bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) override;
|
||||
isminetype IsMine(const CScript& script) const override;
|
||||
|
||||
bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) override;
|
||||
bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) override;
|
||||
|
||||
bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool) override;
|
||||
void ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) override;
|
||||
|
||||
// Tops up the descriptor cache and m_map_script_pub_keys. The cache is stored in the wallet file
|
||||
// and is used to expand the descriptor in GetNewDestination. DescriptorScriptPubKeyMan relies
|
||||
// more on ephemeral data than LegacyScriptPubKeyMan. For wallets using unhardened derivation
|
||||
// (with or without private keys), the "keypool" is a single xpub.
|
||||
bool TopUp(unsigned int size = 0) override;
|
||||
|
||||
void MarkUnusedAddresses(const CScript& script) override;
|
||||
|
||||
bool IsHDEnabled() const override;
|
||||
|
||||
//! Setup descriptors based on the given CExtkey
|
||||
bool SetupDescriptorGeneration(const CExtKey& master_key);
|
||||
|
||||
bool HavePrivateKeys() const override;
|
||||
|
||||
int64_t GetOldestKeyPoolTime() const override;
|
||||
size_t KeypoolCountExternalKeys() const override;
|
||||
unsigned int GetKeyPoolSize() const override;
|
||||
|
||||
int64_t GetTimeFirstKey() const override;
|
||||
|
||||
std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const override;
|
||||
|
||||
bool CanGetAddresses(bool internal = false) const override;
|
||||
|
||||
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const override;
|
||||
|
||||
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
|
||||
|
||||
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
|
||||
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
|
||||
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const override;
|
||||
|
||||
uint256 GetID() const override;
|
||||
|
||||
void SetType(OutputType type, bool internal) override;
|
||||
|
||||
void SetCache(const DescriptorCache& cache);
|
||||
|
||||
bool AddKey(const CKeyID& key_id, const CKey& key);
|
||||
bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key);
|
||||
|
||||
bool HasWalletDescriptor(const WalletDescriptor& desc) const;
|
||||
void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
|
||||
void WriteDescriptor();
|
||||
|
||||
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
const std::vector<CScript> GetScriptPubKeys() const;
|
||||
};
|
||||
|
||||
#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
|
||||
|
|
|
@ -637,4 +637,25 @@ BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
|
|||
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
|
||||
}
|
||||
|
||||
bool malformed_descriptor(std::ios_base::failure e)
|
||||
{
|
||||
std::string s(e.what());
|
||||
return s.find("Missing checksum") != std::string::npos;
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
|
||||
{
|
||||
std::vector<unsigned char> malformed_record;
|
||||
CVectorWriter vw(0, 0, malformed_record, 0);
|
||||
vw << std::string("notadescriptor");
|
||||
vw << (uint64_t)0;
|
||||
vw << (int32_t)0;
|
||||
vw << (int32_t)0;
|
||||
vw << (int32_t)1;
|
||||
|
||||
VectorReader vr(0, 0, malformed_record, 0);
|
||||
WalletDescriptor w_desc;
|
||||
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
|
|
@ -228,10 +228,14 @@ WalletCreationStatus CreateWallet(interfaces::Chain& chain, const SecureString&
|
|||
// Set a seed for the wallet
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
for (auto spk_man : wallet->GetActiveScriptPubKeyMans()) {
|
||||
if (!spk_man->SetupGeneration()) {
|
||||
error = "Unable to generate initial keys";
|
||||
return WalletCreationStatus::CREATION_FAILED;
|
||||
if (wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
wallet->SetupDescriptorScriptPubKeyMans();
|
||||
} else {
|
||||
for (auto spk_man : wallet->GetActiveScriptPubKeyMans()) {
|
||||
if (!spk_man->SetupGeneration()) {
|
||||
error = "Unable to generate initial keys";
|
||||
return WalletCreationStatus::CREATION_FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -588,8 +592,11 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
|
|||
Lock();
|
||||
Unlock(strWalletPassphrase);
|
||||
|
||||
// if we are using HD, replace the HD seed with a new one
|
||||
if (auto spk_man = GetLegacyScriptPubKeyMan()) {
|
||||
// If we are using descriptors, make new descriptors with a new seed
|
||||
if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) && !IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)) {
|
||||
SetupDescriptorScriptPubKeyMans();
|
||||
} else if (auto spk_man = GetLegacyScriptPubKeyMan()) {
|
||||
// if we are using HD, replace the HD seed with a new one
|
||||
if (spk_man->IsHDEnabled()) {
|
||||
if (!spk_man->SetupGeneration(true)) {
|
||||
return false;
|
||||
|
@ -747,22 +754,29 @@ bool CWallet::IsSpentKey(const uint256& hash, unsigned int n) const
|
|||
const CWalletTx* srctx = GetWalletTx(hash);
|
||||
if (srctx) {
|
||||
assert(srctx->tx->vout.size() > n);
|
||||
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
|
||||
// When descriptor wallets arrive, these additional checks are
|
||||
// likely superfluous and can be optimized out
|
||||
assert(spk_man != nullptr);
|
||||
for (const auto& keyid : GetAffectedKeys(srctx->tx->vout[n].scriptPubKey, *spk_man)) {
|
||||
WitnessV0KeyHash wpkh_dest(keyid);
|
||||
if (GetDestData(wpkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
ScriptHash sh_wpkh_dest(GetScriptForDestination(wpkh_dest));
|
||||
if (GetDestData(sh_wpkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
PKHash pkh_dest(keyid);
|
||||
if (GetDestData(pkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
CTxDestination dest;
|
||||
if (!ExtractDestination(srctx->tx->vout[n].scriptPubKey, dest)) {
|
||||
return false;
|
||||
}
|
||||
if (GetDestData(dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
if (IsLegacy()) {
|
||||
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
|
||||
assert(spk_man != nullptr);
|
||||
for (const auto& keyid : GetAffectedKeys(srctx->tx->vout[n].scriptPubKey, *spk_man)) {
|
||||
WitnessV0KeyHash wpkh_dest(keyid);
|
||||
if (GetDestData(wpkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
ScriptHash sh_wpkh_dest(GetScriptForDestination(wpkh_dest));
|
||||
if (GetDestData(sh_wpkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
PKHash pkh_dest(keyid);
|
||||
if (GetDestData(pkh_dest, "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1331,9 +1345,10 @@ CAmount CWallet::GetChange(const CTransaction& tx) const
|
|||
|
||||
bool CWallet::IsHDEnabled() const
|
||||
{
|
||||
// All Active ScriptPubKeyMans must be HD for this to be true
|
||||
bool result = true;
|
||||
for (const auto& spk_man_pair : m_spk_managers) {
|
||||
result &= spk_man_pair.second->IsHDEnabled();
|
||||
for (const auto& spk_man : GetActiveScriptPubKeyMans()) {
|
||||
result &= spk_man->IsHDEnabled();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -2427,11 +2442,17 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const
|
|||
|
||||
bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const
|
||||
{
|
||||
// Sign the tx with ScriptPubKeyMans
|
||||
// Because each ScriptPubKeyMan can sign more than one input, we need to keep track of each ScriptPubKeyMan that has signed this transaction.
|
||||
// Each iteration, we may sign more txins than the txin that is specified in that iteration.
|
||||
// We assume that each input is signed by only one ScriptPubKeyMan.
|
||||
std::set<uint256> visited_spk_mans;
|
||||
// Try to sign with all ScriptPubKeyMans
|
||||
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
|
||||
// spk_man->SignTransaction will return true if the transaction is complete,
|
||||
// so we can exit early and return true if that happens
|
||||
if (spk_man->SignTransaction(tx, coins, sighash, input_errors)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, one input was not fully signed otherwise we would have exited already
|
||||
// Find that input and figure out what went wrong.
|
||||
for (unsigned int i = 0; i < tx.vin.size(); i++) {
|
||||
// Get the prevout
|
||||
CTxIn& txin = tx.vin[i];
|
||||
|
@ -2443,33 +2464,10 @@ bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint,
|
|||
|
||||
// Check if this input is complete
|
||||
SignatureData sigdata = DataFromTransaction(tx, i, coin->second.out);
|
||||
if (sigdata.complete) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Input needs to be signed, find the right ScriptPubKeyMan
|
||||
std::set<ScriptPubKeyMan*> spk_mans = GetScriptPubKeyMans(coin->second.out.scriptPubKey, sigdata);
|
||||
if (spk_mans.size() == 0) {
|
||||
if (!sigdata.complete) {
|
||||
input_errors[i] = "Unable to sign input, missing keys";
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& spk_man : spk_mans) {
|
||||
// If we've already been signed by this spk_man, skip it
|
||||
if (visited_spk_mans.count(spk_man->GetID()) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sign the tx.
|
||||
// spk_man->SignTransaction will return true if the transaction is complete,
|
||||
// so we can exit early and return true if that happens.
|
||||
if (spk_man->SignTransaction(tx, coins, sighash, input_errors)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add this spk_man to visited_spk_mans so we can skip it later
|
||||
visited_spk_mans.insert(spk_man->GetID());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -2505,52 +2503,10 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp
|
|||
}
|
||||
|
||||
// Fill in information from ScriptPubKeyMans
|
||||
// Because each ScriptPubKeyMan may be able to fill more than one input, we need to keep track of each ScriptPubKeyMan that has filled this psbt.
|
||||
// Each iteration, we may fill more inputs than the input that is specified in that iteration.
|
||||
// We assume that each input is filled by only one ScriptPubKeyMan
|
||||
std::set<uint256> visited_spk_mans;
|
||||
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
|
||||
const CTxIn& txin = psbtx.tx->vin[i];
|
||||
PSBTInput& input = psbtx.inputs.at(i);
|
||||
|
||||
if (PSBTInputSigned(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the scriptPubKey to know which ScriptPubKeyMan to use
|
||||
CScript script;
|
||||
if (!input.witness_utxo.IsNull()) {
|
||||
script = input.witness_utxo.scriptPubKey;
|
||||
} else if (input.non_witness_utxo) {
|
||||
if (txin.prevout.n >= input.non_witness_utxo->vout.size()) {
|
||||
return TransactionError::MISSING_INPUTS;
|
||||
}
|
||||
script = input.non_witness_utxo->vout[txin.prevout.n].scriptPubKey;
|
||||
} else {
|
||||
// There's no UTXO so we can just skip this now
|
||||
continue;
|
||||
}
|
||||
SignatureData sigdata;
|
||||
input.FillSignatureData(sigdata);
|
||||
std::set<ScriptPubKeyMan*> spk_mans = GetScriptPubKeyMans(script, sigdata);
|
||||
if (spk_mans.size() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& spk_man : spk_mans) {
|
||||
// If we've already been signed by this spk_man, skip it
|
||||
if (visited_spk_mans.count(spk_man->GetID()) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fill in the information from the spk_man
|
||||
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs);
|
||||
if (res != TransactionError::OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Add this spk_man to visited_spk_mans so we can skip it later
|
||||
visited_spk_mans.insert(spk_man->GetID());
|
||||
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
|
||||
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs);
|
||||
if (res != TransactionError::OK) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3278,6 +3234,8 @@ bool CWallet::GetNewDestination(const OutputType type, const std::string label,
|
|||
if (spk_man) {
|
||||
spk_man->TopUp();
|
||||
result = spk_man->GetNewDestination(type, dest, error);
|
||||
} else {
|
||||
error = strprintf("Error: No %s addresses available.", FormatOutputType(type));
|
||||
}
|
||||
if (result) {
|
||||
SetAddressBook(dest, label, "receive");
|
||||
|
@ -3834,15 +3792,23 @@ std::shared_ptr<CWallet> CWallet::CreateWalletFromFile(interfaces::Chain& chain,
|
|||
|
||||
walletInstance->SetWalletFlags(wallet_creation_flags, false);
|
||||
|
||||
// Always create LegacyScriptPubKeyMan for now
|
||||
walletInstance->SetupLegacyScriptPubKeyMan();
|
||||
// Only create LegacyScriptPubKeyMan when not descriptor wallet
|
||||
if (!walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
walletInstance->SetupLegacyScriptPubKeyMan();
|
||||
}
|
||||
|
||||
if (!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
|
||||
LOCK(walletInstance->cs_wallet);
|
||||
for (auto spk_man : walletInstance->GetActiveScriptPubKeyMans()) {
|
||||
if (!spk_man->SetupGeneration()) {
|
||||
error = _("Unable to generate initial keys").translated;
|
||||
return nullptr;
|
||||
if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
walletInstance->SetupDescriptorScriptPubKeyMans();
|
||||
// SetupDescriptorScriptPubKeyMans already calls SetupGeneration for us so we don't need to call SetupGeneration separately
|
||||
} else {
|
||||
// Legacy wallets need SetupGeneration here.
|
||||
for (auto spk_man : walletInstance->GetActiveScriptPubKeyMans()) {
|
||||
if (!spk_man->SetupGeneration()) {
|
||||
error = _("Unable to generate initial keys").translated;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4353,6 +4319,9 @@ std::unique_ptr<SigningProvider> CWallet::GetSolvingProvider(const CScript& scri
|
|||
|
||||
LegacyScriptPubKeyMan* CWallet::GetLegacyScriptPubKeyMan() const
|
||||
{
|
||||
if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
return nullptr;
|
||||
}
|
||||
// Legacy wallets only have one ScriptPubKeyMan which is a LegacyScriptPubKeyMan.
|
||||
// Everything in m_internal_spk_managers and m_external_spk_managers point to the same legacyScriptPubKeyMan.
|
||||
auto it = m_internal_spk_managers.find(OutputType::LEGACY);
|
||||
|
@ -4368,7 +4337,7 @@ LegacyScriptPubKeyMan* CWallet::GetOrCreateLegacyScriptPubKeyMan()
|
|||
|
||||
void CWallet::SetupLegacyScriptPubKeyMan()
|
||||
{
|
||||
if (!m_internal_spk_managers.empty() || !m_external_spk_managers.empty() || !m_spk_managers.empty()) {
|
||||
if (!m_internal_spk_managers.empty() || !m_external_spk_managers.empty() || !m_spk_managers.empty() || IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4397,3 +4366,153 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
|
|||
spk_man->NotifyCanGetAddressesChanged.connect(NotifyCanGetAddressesChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc)
|
||||
{
|
||||
auto spk_manager = std::unique_ptr<ScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, desc));
|
||||
m_spk_managers[id] = std::move(spk_manager);
|
||||
}
|
||||
|
||||
void CWallet::SetupDescriptorScriptPubKeyMans()
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
// Make a seed
|
||||
CKey seed_key;
|
||||
seed_key.MakeNewKey(true);
|
||||
CPubKey seed = seed_key.GetPubKey();
|
||||
assert(seed_key.VerifyPubKey(seed));
|
||||
|
||||
// Get the extended key
|
||||
CExtKey master_key;
|
||||
master_key.SetSeed(seed_key.begin(), seed_key.size());
|
||||
|
||||
for (bool internal : {false, true}) {
|
||||
for (OutputType t : OUTPUT_TYPES) {
|
||||
auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, t, internal));
|
||||
if (IsCrypted()) {
|
||||
if (IsLocked()) {
|
||||
throw std::runtime_error(std::string(__func__) + ": Wallet is locked, cannot setup new descriptors");
|
||||
}
|
||||
if (!spk_manager->CheckDecryptionKey(vMasterKey) && !spk_manager->Encrypt(vMasterKey, nullptr)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors");
|
||||
}
|
||||
}
|
||||
spk_manager->SetupDescriptorGeneration(master_key);
|
||||
uint256 id = spk_manager->GetID();
|
||||
m_spk_managers[id] = std::move(spk_manager);
|
||||
SetActiveScriptPubKeyMan(id, t, internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CWallet::SetActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal, bool memonly)
|
||||
{
|
||||
WalletLogPrintf("Setting spkMan to active: id = %s, type = %d, internal = %d\n", id.ToString(), static_cast<int>(type), static_cast<int>(internal));
|
||||
auto& spk_mans = internal ? m_internal_spk_managers : m_external_spk_managers;
|
||||
auto spk_man = m_spk_managers.at(id).get();
|
||||
spk_man->SetType(type, internal);
|
||||
spk_mans[type] = spk_man;
|
||||
|
||||
if (!memonly) {
|
||||
WalletBatch batch(*database);
|
||||
if (!batch.WriteActiveScriptPubKeyMan(static_cast<uint8_t>(type), id, internal)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing active ScriptPubKeyMan id failed");
|
||||
}
|
||||
}
|
||||
NotifyCanGetAddressesChanged();
|
||||
}
|
||||
|
||||
bool CWallet::IsLegacy() const
|
||||
{
|
||||
if (m_internal_spk_managers.count(OutputType::LEGACY) == 0) {
|
||||
return false;
|
||||
}
|
||||
auto spk_man = dynamic_cast<LegacyScriptPubKeyMan*>(m_internal_spk_managers.at(OutputType::LEGACY));
|
||||
return spk_man != nullptr;
|
||||
}
|
||||
|
||||
DescriptorScriptPubKeyMan* CWallet::GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const
|
||||
{
|
||||
for (auto& spk_man_pair : m_spk_managers) {
|
||||
// Try to downcast to DescriptorScriptPubKeyMan then check if the descriptors match
|
||||
DescriptorScriptPubKeyMan* spk_manager = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man_pair.second.get());
|
||||
if (spk_manager != nullptr && spk_manager->HasWalletDescriptor(desc)) {
|
||||
return spk_manager;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label)
|
||||
{
|
||||
if (!IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
WalletLogPrintf("Cannot add WalletDescriptor to a non-descriptor wallet\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LOCK(cs_wallet);
|
||||
auto new_spk_man = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, desc));
|
||||
|
||||
// If we already have this descriptor, remove it from the maps but add the existing cache to desc
|
||||
auto old_spk_man = GetDescriptorScriptPubKeyMan(desc);
|
||||
if (old_spk_man) {
|
||||
WalletLogPrintf("Update existing descriptor: %s\n", desc.descriptor->ToString());
|
||||
|
||||
{
|
||||
LOCK(old_spk_man->cs_desc_man);
|
||||
new_spk_man->SetCache(old_spk_man->GetWalletDescriptor().cache);
|
||||
}
|
||||
|
||||
// Remove from maps of active spkMans
|
||||
auto old_spk_man_id = old_spk_man->GetID();
|
||||
for (bool internal : {false, true}) {
|
||||
for (OutputType t : OUTPUT_TYPES) {
|
||||
auto active_spk_man = GetScriptPubKeyMan(t, internal);
|
||||
if (active_spk_man && active_spk_man->GetID() == old_spk_man_id) {
|
||||
if (internal) {
|
||||
m_internal_spk_managers.erase(t);
|
||||
} else {
|
||||
m_external_spk_managers.erase(t);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_spk_managers.erase(old_spk_man_id);
|
||||
}
|
||||
|
||||
// Add the private keys to the descriptor
|
||||
for (const auto& entry : signing_provider.keys) {
|
||||
const CKey& key = entry.second;
|
||||
new_spk_man->AddDescriptorKey(key, key.GetPubKey());
|
||||
}
|
||||
|
||||
// Top up key pool, the manager will generate new scriptPubKeys internally
|
||||
new_spk_man->TopUp();
|
||||
|
||||
// Apply the label if necessary
|
||||
// Note: we disable labels for ranged descriptors
|
||||
if (!desc.descriptor->IsRange()) {
|
||||
auto script_pub_keys = new_spk_man->GetScriptPubKeys();
|
||||
if (script_pub_keys.empty()) {
|
||||
WalletLogPrintf("Could not generate scriptPubKeys (cache is empty)\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CTxDestination dest;
|
||||
if (ExtractDestination(script_pub_keys.at(0), dest)) {
|
||||
SetAddressBook(dest, label, "receive");
|
||||
}
|
||||
}
|
||||
|
||||
// Save the descriptor to memory
|
||||
auto ret = new_spk_man.get();
|
||||
m_spk_managers[new_spk_man->GetID()] = std::move(new_spk_man);
|
||||
|
||||
// Save the descriptor to DB
|
||||
ret->WriteDescriptor();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -111,7 +111,8 @@ static constexpr uint64_t KNOWN_WALLET_FLAGS =
|
|||
WALLET_FLAG_AVOID_REUSE
|
||||
| WALLET_FLAG_BLANK_WALLET
|
||||
| WALLET_FLAG_KEY_ORIGIN_METADATA
|
||||
| WALLET_FLAG_DISABLE_PRIVATE_KEYS;
|
||||
| WALLET_FLAG_DISABLE_PRIVATE_KEYS
|
||||
| WALLET_FLAG_DESCRIPTORS;
|
||||
|
||||
static constexpr uint64_t MUTABLE_WALLET_FLAGS =
|
||||
WALLET_FLAG_AVOID_REUSE;
|
||||
|
@ -121,6 +122,7 @@ static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
|
|||
{"blank", WALLET_FLAG_BLANK_WALLET},
|
||||
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
|
||||
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
|
||||
{"descriptor_wallet", WALLET_FLAG_DESCRIPTORS},
|
||||
};
|
||||
|
||||
extern const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS;
|
||||
|
@ -1163,6 +1165,9 @@ public:
|
|||
returns false if unknown, non-tolerable flags are present */
|
||||
bool SetWalletFlags(uint64_t overwriteFlags, bool memOnly);
|
||||
|
||||
/** Determine if we are a legacy wallet */
|
||||
bool IsLegacy() const;
|
||||
|
||||
/** Returns a bracketed wallet name for displaying in logs, will return [default wallet] if the wallet has no name */
|
||||
const std::string GetDisplayName() const override {
|
||||
std::string wallet_name = GetName().length() == 0 ? "default wallet" : GetName();
|
||||
|
@ -1232,6 +1237,25 @@ public:
|
|||
|
||||
//! Connect the signals from ScriptPubKeyMans to the signals in CWallet
|
||||
void ConnectScriptPubKeyManNotifiers();
|
||||
|
||||
//! Instantiate a descriptor ScriptPubKeyMan from the WalletDescriptor and load it
|
||||
void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc);
|
||||
|
||||
//! Sets the active ScriptPubKeyMan for the specified type and internal
|
||||
//! @param[in] id The unique id for the ScriptPubKeyMan
|
||||
//! @param[in] type The OutputType this ScriptPubKeyMan provides addresses for
|
||||
//! @param[in] internal Whether this ScriptPubKeyMan provides change addresses
|
||||
//! @param[in] memonly Whether to record this update to the database. Set to true for wallet loading, normally false when actually updating the wallet.
|
||||
void SetActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal, bool memonly = false);
|
||||
|
||||
//! Create new DescriptorScriptPubKeyMans and add them to the wallet
|
||||
void SetupDescriptorScriptPubKeyMans();
|
||||
|
||||
//! Return the DescriptorScriptPubKeyMan for a WalletDescriptor if it is already in the wallet
|
||||
DescriptorScriptPubKeyMan* GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const;
|
||||
|
||||
//! Add a descriptor to the wallet, return a ScriptPubKeyMan & associated output type
|
||||
ScriptPubKeyMan* AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
namespace DBKeys {
|
||||
const std::string ACENTRY{"acentry"};
|
||||
const std::string ACTIVEEXTERNALSPK{"activeexternalspk"};
|
||||
const std::string ACTIVEINTERNALSPK{"activeinternalspk"};
|
||||
const std::string BESTBLOCK_NOMERKLE{"bestblock_nomerkle"};
|
||||
const std::string BESTBLOCK{"bestblock"};
|
||||
const std::string CRYPTED_KEY{"ckey"};
|
||||
|
@ -41,6 +43,10 @@ const std::string PURPOSE{"purpose"};
|
|||
const std::string SETTINGS{"settings"};
|
||||
const std::string TX{"tx"};
|
||||
const std::string VERSION{"version"};
|
||||
const std::string WALLETDESCRIPTOR{"walletdescriptor"};
|
||||
const std::string WALLETDESCRIPTORCACHE{"walletdescriptorcache"};
|
||||
const std::string WALLETDESCRIPTORCKEY{"walletdescriptorckey"};
|
||||
const std::string WALLETDESCRIPTORKEY{"walletdescriptorkey"};
|
||||
const std::string WATCHMETA{"watchmeta"};
|
||||
const std::string WATCHS{"watchs"};
|
||||
} // namespace DBKeys
|
||||
|
@ -179,6 +185,51 @@ bool WalletBatch::WriteMinVersion(int nVersion)
|
|||
return WriteIC(DBKeys::MINVERSION, nVersion);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal)
|
||||
{
|
||||
std::string key = internal ? DBKeys::ACTIVEINTERNALSPK : DBKeys::ACTIVEEXTERNALSPK;
|
||||
return WriteIC(make_pair(key, type), id);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey)
|
||||
{
|
||||
// hash pubkey/privkey to accelerate wallet load
|
||||
std::vector<unsigned char> key;
|
||||
key.reserve(pubkey.size() + privkey.size());
|
||||
key.insert(key.end(), pubkey.begin(), pubkey.end());
|
||||
key.insert(key.end(), privkey.begin(), privkey.end());
|
||||
|
||||
return WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORKEY, std::make_pair(desc_id, pubkey)), std::make_pair(privkey, Hash(key.begin(), key.end())), false);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector<unsigned char>& secret)
|
||||
{
|
||||
if (!WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORCKEY, std::make_pair(desc_id, pubkey)), secret, false)) {
|
||||
return false;
|
||||
}
|
||||
EraseIC(std::make_pair(DBKeys::WALLETDESCRIPTORKEY, std::make_pair(desc_id, pubkey)));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor)
|
||||
{
|
||||
return WriteIC(make_pair(DBKeys::WALLETDESCRIPTOR, desc_id), descriptor);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteDescriptorDerivedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index, uint32_t der_index)
|
||||
{
|
||||
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
|
||||
xpub.Encode(ser_xpub.data());
|
||||
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), std::make_pair(key_exp_index, der_index)), ser_xpub);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteDescriptorParentCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index)
|
||||
{
|
||||
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
|
||||
xpub.Encode(ser_xpub.data());
|
||||
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), key_exp_index), ser_xpub);
|
||||
}
|
||||
|
||||
class CWalletScanState {
|
||||
public:
|
||||
unsigned int nKeys{0};
|
||||
|
@ -189,6 +240,11 @@ public:
|
|||
bool fIsEncrypted{false};
|
||||
bool fAnyUnordered{false};
|
||||
std::vector<uint256> vWalletUpgrade;
|
||||
std::map<OutputType, uint256> m_active_external_spks;
|
||||
std::map<OutputType, uint256> m_active_internal_spks;
|
||||
std::map<uint256, DescriptorCache> m_descriptor_caches;
|
||||
std::map<std::pair<uint256, CKeyID>, CKey> m_descriptor_keys;
|
||||
std::map<std::pair<uint256, CKeyID>, std::pair<CPubKey, std::vector<unsigned char>>> m_descriptor_crypt_keys;
|
||||
|
||||
CWalletScanState() {
|
||||
}
|
||||
|
@ -404,6 +460,108 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||
} else if (strType == DBKeys::OLD_KEY) {
|
||||
strErr = "Found unsupported 'wkey' record, try loading with version 0.18";
|
||||
return false;
|
||||
} else if (strType == DBKeys::ACTIVEEXTERNALSPK || strType == DBKeys::ACTIVEINTERNALSPK) {
|
||||
uint8_t type;
|
||||
ssKey >> type;
|
||||
uint256 id;
|
||||
ssValue >> id;
|
||||
|
||||
bool internal = strType == DBKeys::ACTIVEINTERNALSPK;
|
||||
auto& spk_mans = internal ? wss.m_active_internal_spks : wss.m_active_external_spks;
|
||||
if (spk_mans.count(static_cast<OutputType>(type)) > 0) {
|
||||
strErr = "Multiple ScriptPubKeyMans specified for a single type";
|
||||
return false;
|
||||
}
|
||||
spk_mans[static_cast<OutputType>(type)] = id;
|
||||
} else if (strType == DBKeys::WALLETDESCRIPTOR) {
|
||||
uint256 id;
|
||||
ssKey >> id;
|
||||
WalletDescriptor desc;
|
||||
ssValue >> desc;
|
||||
if (wss.m_descriptor_caches.count(id) == 0) {
|
||||
wss.m_descriptor_caches[id] = DescriptorCache();
|
||||
}
|
||||
pwallet->LoadDescriptorScriptPubKeyMan(id, desc);
|
||||
} else if (strType == DBKeys::WALLETDESCRIPTORCACHE) {
|
||||
bool parent = true;
|
||||
uint256 desc_id;
|
||||
uint32_t key_exp_index;
|
||||
uint32_t der_index;
|
||||
ssKey >> desc_id;
|
||||
ssKey >> key_exp_index;
|
||||
|
||||
// if the der_index exists, it's a derived xpub
|
||||
try
|
||||
{
|
||||
ssKey >> der_index;
|
||||
parent = false;
|
||||
}
|
||||
catch (...) {}
|
||||
|
||||
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
|
||||
ssValue >> ser_xpub;
|
||||
CExtPubKey xpub;
|
||||
xpub.Decode(ser_xpub.data());
|
||||
if (wss.m_descriptor_caches.count(desc_id)) {
|
||||
wss.m_descriptor_caches[desc_id] = DescriptorCache();
|
||||
}
|
||||
if (parent) {
|
||||
wss.m_descriptor_caches[desc_id].CacheParentExtPubKey(key_exp_index, xpub);
|
||||
} else {
|
||||
wss.m_descriptor_caches[desc_id].CacheDerivedExtPubKey(key_exp_index, der_index, xpub);
|
||||
}
|
||||
} else if (strType == DBKeys::WALLETDESCRIPTORKEY) {
|
||||
uint256 desc_id;
|
||||
CPubKey pubkey;
|
||||
ssKey >> desc_id;
|
||||
ssKey >> pubkey;
|
||||
if (!pubkey.IsValid())
|
||||
{
|
||||
strErr = "Error reading wallet database: CPubKey corrupt";
|
||||
return false;
|
||||
}
|
||||
CKey key;
|
||||
CPrivKey pkey;
|
||||
uint256 hash;
|
||||
|
||||
wss.nKeys++;
|
||||
ssValue >> pkey;
|
||||
ssValue >> hash;
|
||||
|
||||
// hash pubkey/privkey to accelerate wallet load
|
||||
std::vector<unsigned char> to_hash;
|
||||
to_hash.reserve(pubkey.size() + pkey.size());
|
||||
to_hash.insert(to_hash.end(), pubkey.begin(), pubkey.end());
|
||||
to_hash.insert(to_hash.end(), pkey.begin(), pkey.end());
|
||||
|
||||
if (Hash(to_hash.begin(), to_hash.end()) != hash)
|
||||
{
|
||||
strErr = "Error reading wallet database: CPubKey/CPrivKey corrupt";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!key.Load(pkey, pubkey, true))
|
||||
{
|
||||
strErr = "Error reading wallet database: CPrivKey corrupt";
|
||||
return false;
|
||||
}
|
||||
wss.m_descriptor_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), key));
|
||||
} else if (strType == DBKeys::WALLETDESCRIPTORCKEY) {
|
||||
uint256 desc_id;
|
||||
CPubKey pubkey;
|
||||
ssKey >> desc_id;
|
||||
ssKey >> pubkey;
|
||||
if (!pubkey.IsValid())
|
||||
{
|
||||
strErr = "Error reading wallet database: CPubKey corrupt";
|
||||
return false;
|
||||
}
|
||||
std::vector<unsigned char> privkey;
|
||||
ssValue >> privkey;
|
||||
wss.nCKeys++;
|
||||
|
||||
wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey)));
|
||||
wss.fIsEncrypted = true;
|
||||
} else if (strType != DBKeys::BESTBLOCK && strType != DBKeys::BESTBLOCK_NOMERKLE &&
|
||||
strType != DBKeys::MINVERSION && strType != DBKeys::ACENTRY &&
|
||||
strType != DBKeys::VERSION && strType != DBKeys::SETTINGS) {
|
||||
|
@ -497,6 +655,31 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
|
|||
result = DBErrors::CORRUPT;
|
||||
}
|
||||
|
||||
// Set the active ScriptPubKeyMans
|
||||
for (auto spk_man_pair : wss.m_active_external_spks) {
|
||||
pwallet->SetActiveScriptPubKeyMan(spk_man_pair.second, spk_man_pair.first, /* internal */ false, /* memonly */ true);
|
||||
}
|
||||
for (auto spk_man_pair : wss.m_active_internal_spks) {
|
||||
pwallet->SetActiveScriptPubKeyMan(spk_man_pair.second, spk_man_pair.first, /* internal */ true, /* memonly */ true);
|
||||
}
|
||||
|
||||
// Set the descriptor caches
|
||||
for (auto desc_cache_pair : wss.m_descriptor_caches) {
|
||||
auto spk_man = pwallet->GetScriptPubKeyMan(desc_cache_pair.first);
|
||||
assert(spk_man);
|
||||
((DescriptorScriptPubKeyMan*)spk_man)->SetCache(desc_cache_pair.second);
|
||||
}
|
||||
|
||||
// Set the descriptor keys
|
||||
for (auto desc_key_pair : wss.m_descriptor_keys) {
|
||||
auto spk_man = pwallet->GetScriptPubKeyMan(desc_key_pair.first.first);
|
||||
((DescriptorScriptPubKeyMan*)spk_man)->AddKey(desc_key_pair.first.second, desc_key_pair.second);
|
||||
}
|
||||
for (auto desc_key_pair : wss.m_descriptor_crypt_keys) {
|
||||
auto spk_man = pwallet->GetScriptPubKeyMan(desc_key_pair.first.first);
|
||||
((DescriptorScriptPubKeyMan*)spk_man)->AddCryptedKey(desc_key_pair.first.second, desc_key_pair.second.first, desc_key_pair.second.second);
|
||||
}
|
||||
|
||||
if (fNoncriticalErrors && result == DBErrors::LOAD_OK)
|
||||
result = DBErrors::NONCRITICAL_ERROR;
|
||||
|
||||
|
@ -516,7 +699,7 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
|
|||
wss.nKeys, wss.nCKeys, wss.nKeyMeta, wss.nKeys + wss.nCKeys, wss.m_unknown_records);
|
||||
|
||||
// nTimeFirstKey is only reliable if all keys have metadata
|
||||
if ((wss.nKeys + wss.nCKeys + wss.nWatchKeys) != wss.nKeyMeta) {
|
||||
if (pwallet->IsLegacy() && (wss.nKeys + wss.nCKeys + wss.nWatchKeys) != wss.nKeyMeta) {
|
||||
auto spk_man = pwallet->GetOrCreateLegacyScriptPubKeyMan();
|
||||
if (spk_man) {
|
||||
LOCK(spk_man->cs_KeyStore);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <amount.h>
|
||||
#include <script/sign.h>
|
||||
#include <wallet/db.h>
|
||||
#include <wallet/walletutil.h>
|
||||
#include <key.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
@ -54,6 +55,8 @@ enum class DBErrors
|
|||
|
||||
namespace DBKeys {
|
||||
extern const std::string ACENTRY;
|
||||
extern const std::string ACTIVEEXTERNALSPK;
|
||||
extern const std::string ACTIVEINTERNALSPK;
|
||||
extern const std::string BESTBLOCK;
|
||||
extern const std::string BESTBLOCK_NOMERKLE;
|
||||
extern const std::string CRYPTED_KEY;
|
||||
|
@ -74,6 +77,9 @@ extern const std::string PURPOSE;
|
|||
extern const std::string SETTINGS;
|
||||
extern const std::string TX;
|
||||
extern const std::string VERSION;
|
||||
extern const std::string WALLETDESCRIPTOR;
|
||||
extern const std::string WALLETDESCRIPTORCKEY;
|
||||
extern const std::string WALLETDESCRIPTORKEY;
|
||||
extern const std::string WATCHMETA;
|
||||
extern const std::string WATCHS;
|
||||
} // namespace DBKeys
|
||||
|
@ -240,11 +246,19 @@ public:
|
|||
|
||||
bool WriteMinVersion(int nVersion);
|
||||
|
||||
bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey);
|
||||
bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector<unsigned char>& secret);
|
||||
bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor);
|
||||
bool WriteDescriptorDerivedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index, uint32_t der_index);
|
||||
bool WriteDescriptorParentCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
|
||||
|
||||
/// Write destination data key,value tuple to database
|
||||
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
|
||||
/// Erase destination data tuple from wallet database
|
||||
bool EraseDestData(const std::string &address, const std::string &key);
|
||||
|
||||
bool WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal);
|
||||
|
||||
DBErrors LoadWallet(CWallet* pwallet);
|
||||
DBErrors FindWalletTx(std::vector<uint256>& vTxHash, std::vector<CWalletTx>& vWtx);
|
||||
DBErrors ZapWalletTx(std::vector<CWalletTx>& vWtx);
|
||||
|
|
|
@ -100,5 +100,10 @@ WalletLocation::WalletLocation(const std::string& name)
|
|||
|
||||
bool WalletLocation::Exists() const
|
||||
{
|
||||
return fs::symlink_status(m_path).type() != fs::file_not_found;
|
||||
fs::path path = m_path;
|
||||
// For the default wallet, check specifically for the wallet.dat file
|
||||
if (m_name.empty()) {
|
||||
path = fs::absolute("wallet.dat", m_path);
|
||||
}
|
||||
return fs::symlink_status(path).type() != fs::file_not_found;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#define BITCOIN_WALLET_WALLETUTIL_H
|
||||
|
||||
#include <fs.h>
|
||||
#include <script/descriptor.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
|
@ -55,6 +56,9 @@ enum WalletFlags : uint64_t {
|
|||
//! bitcoin from opening the wallet, thinking it was newly created, and
|
||||
//! then improperly reinitializing it.
|
||||
WALLET_FLAG_BLANK_WALLET = (1ULL << 33),
|
||||
|
||||
//! Indicate that this wallet supports DescriptorScriptPubKeyMan
|
||||
WALLET_FLAG_DESCRIPTORS = (1ULL << 34),
|
||||
};
|
||||
|
||||
//! Get the path of the wallet directory.
|
||||
|
@ -83,4 +87,41 @@ public:
|
|||
bool Exists() const;
|
||||
};
|
||||
|
||||
/** Descriptor with some wallet metadata */
|
||||
class WalletDescriptor
|
||||
{
|
||||
public:
|
||||
std::shared_ptr<Descriptor> descriptor;
|
||||
uint64_t creation_time;
|
||||
int32_t range_start; // First item in range; start of range, inclusive, i.e. [range_start, range_end). This never changes.
|
||||
int32_t range_end; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
|
||||
int32_t next_index; // Position of the next item to generate
|
||||
DescriptorCache cache;
|
||||
|
||||
ADD_SERIALIZE_METHODS;
|
||||
|
||||
template <typename Stream, typename Operation>
|
||||
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||
if (ser_action.ForRead()) {
|
||||
std::string desc;
|
||||
std::string error;
|
||||
READWRITE(desc);
|
||||
FlatSigningProvider keys;
|
||||
descriptor = Parse(desc, keys, error, true);
|
||||
if (!descriptor) {
|
||||
throw std::ios_base::failure("Invalid descriptor: " + error);
|
||||
}
|
||||
} else {
|
||||
READWRITE(descriptor->ToString());
|
||||
}
|
||||
READWRITE(creation_time);
|
||||
READWRITE(next_index);
|
||||
READWRITE(range_start);
|
||||
READWRITE(range_end);
|
||||
}
|
||||
|
||||
WalletDescriptor() {}
|
||||
WalletDescriptor(std::shared_ptr<Descriptor> descriptor, uint64_t creation_time, int32_t range_start, int32_t range_end, int32_t next_index) : descriptor(descriptor), creation_time(creation_time), range_start(range_start), range_end(range_end), next_index(next_index) {}
|
||||
};
|
||||
|
||||
#endif // BITCOIN_WALLET_WALLETUTIL_H
|
||||
|
|
|
@ -94,7 +94,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
|
||||
# w1: regular wallet, created on master: update this test when default
|
||||
# wallets can no longer be opened by older versions.
|
||||
node_master.createwallet(wallet_name="w1")
|
||||
node_master.rpc.createwallet(wallet_name="w1")
|
||||
wallet = node_master.get_wallet_rpc("w1")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
|
@ -120,17 +120,17 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
self.nodes[1].abandontransaction(tx3_id)
|
||||
|
||||
# w1_v19: regular wallet, created with v0.19
|
||||
node_v19.createwallet(wallet_name="w1_v19")
|
||||
node_v19.rpc.createwallet(wallet_name="w1_v19")
|
||||
wallet = node_v19.get_wallet_rpc("w1_v19")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
assert info['keypoolsize'] > 0
|
||||
# Use addmultisigaddress (see #18075)
|
||||
address_18075 = wallet.addmultisigaddress(1, ["0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52", "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073"], "", "legacy")["address"]
|
||||
address_18075 = wallet.rpc.addmultisigaddress(1, ["0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52", "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073"], "", "legacy")["address"]
|
||||
assert wallet.getaddressinfo(address_18075)["solvable"]
|
||||
|
||||
# w1_v18: regular wallet, created with v0.18
|
||||
node_v18.createwallet(wallet_name="w1_v18")
|
||||
node_v18.rpc.createwallet(wallet_name="w1_v18")
|
||||
wallet = node_v18.get_wallet_rpc("w1_v18")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
|
@ -139,21 +139,21 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
# w2: wallet with private keys disabled, created on master: update this
|
||||
# test when default wallets private keys disabled can no longer be
|
||||
# opened by older versions.
|
||||
node_master.createwallet(wallet_name="w2", disable_private_keys=True)
|
||||
node_master.rpc.createwallet(wallet_name="w2", disable_private_keys=True)
|
||||
wallet = node_master.get_wallet_rpc("w2")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled'] == False
|
||||
assert info['keypoolsize'] == 0
|
||||
|
||||
# w2_v19: wallet with private keys disabled, created with v0.19
|
||||
node_v19.createwallet(wallet_name="w2_v19", disable_private_keys=True)
|
||||
node_v19.rpc.createwallet(wallet_name="w2_v19", disable_private_keys=True)
|
||||
wallet = node_v19.get_wallet_rpc("w2_v19")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled'] == False
|
||||
assert info['keypoolsize'] == 0
|
||||
|
||||
# w2_v18: wallet with private keys disabled, created with v0.18
|
||||
node_v18.createwallet(wallet_name="w2_v18", disable_private_keys=True)
|
||||
node_v18.rpc.createwallet(wallet_name="w2_v18", disable_private_keys=True)
|
||||
wallet = node_v18.get_wallet_rpc("w2_v18")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled'] == False
|
||||
|
@ -161,21 +161,21 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
|
||||
# w3: blank wallet, created on master: update this
|
||||
# test when default blank wallets can no longer be opened by older versions.
|
||||
node_master.createwallet(wallet_name="w3", blank=True)
|
||||
node_master.rpc.createwallet(wallet_name="w3", blank=True)
|
||||
wallet = node_master.get_wallet_rpc("w3")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
assert info['keypoolsize'] == 0
|
||||
|
||||
# w3_v19: blank wallet, created with v0.19
|
||||
node_v19.createwallet(wallet_name="w3_v19", blank=True)
|
||||
node_v19.rpc.createwallet(wallet_name="w3_v19", blank=True)
|
||||
wallet = node_v19.get_wallet_rpc("w3_v19")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
assert info['keypoolsize'] == 0
|
||||
|
||||
# w3_v18: blank wallet, created with v0.18
|
||||
node_v18.createwallet(wallet_name="w3_v18", blank=True)
|
||||
node_v18.rpc.createwallet(wallet_name="w3_v18", blank=True)
|
||||
wallet = node_v18.get_wallet_rpc("w3_v18")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
|
@ -318,7 +318,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Test wallet upgrade path...")
|
||||
# u1: regular wallet, created with v0.17
|
||||
node_v17.createwallet(wallet_name="u1_v17")
|
||||
node_v17.rpc.createwallet(wallet_name="u1_v17")
|
||||
wallet = node_v17.get_wallet_rpc("u1_v17")
|
||||
address = wallet.getnewaddress("bech32")
|
||||
info = wallet.getaddressinfo(address)
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test multisig RPCs"""
|
||||
|
||||
from test_framework.authproxy import JSONRPCException
|
||||
from test_framework.descriptors import descsum_create, drop_origins
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_raises_rpc_error,
|
||||
assert_equal,
|
||||
)
|
||||
from test_framework.key import ECPubKey
|
||||
from test_framework.key import ECPubKey, ECKey, bytes_to_wif
|
||||
|
||||
import binascii
|
||||
import decimal
|
||||
|
@ -28,10 +29,14 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
self.skip_if_no_wallet()
|
||||
|
||||
def get_keys(self):
|
||||
self.pub = []
|
||||
self.priv = []
|
||||
node0, node1, node2 = self.nodes
|
||||
add = [node1.getnewaddress() for _ in range(self.nkeys)]
|
||||
self.pub = [node1.getaddressinfo(a)["pubkey"] for a in add]
|
||||
self.priv = [node1.dumpprivkey(a) for a in add]
|
||||
for _ in range(self.nkeys):
|
||||
k = ECKey()
|
||||
k.generate()
|
||||
self.pub.append(k.get_pubkey().get_bytes().hex())
|
||||
self.priv.append(bytes_to_wif(k.get_bytes(), k.is_compressed))
|
||||
self.final = node2.getnewaddress()
|
||||
|
||||
def run_test(self):
|
||||
|
@ -64,17 +69,20 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
pk_obj.compressed = False
|
||||
pk2 = binascii.hexlify(pk_obj.get_bytes()).decode()
|
||||
|
||||
node0.createwallet(wallet_name='wmulti0', disable_private_keys=True)
|
||||
wmulti0 = node0.get_wallet_rpc('wmulti0')
|
||||
|
||||
# Check all permutations of keys because order matters apparently
|
||||
for keys in itertools.permutations([pk0, pk1, pk2]):
|
||||
# Results should be the same as this legacy one
|
||||
legacy_addr = node0.createmultisig(2, keys, 'legacy')['address']
|
||||
assert_equal(legacy_addr, node0.addmultisigaddress(2, keys, '', 'legacy')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'legacy')['address'])
|
||||
|
||||
# Generate addresses with the segwit types. These should all make legacy addresses
|
||||
assert_equal(legacy_addr, node0.createmultisig(2, keys, 'bech32')['address'])
|
||||
assert_equal(legacy_addr, node0.createmultisig(2, keys, 'p2sh-segwit')['address'])
|
||||
assert_equal(legacy_addr, node0.addmultisigaddress(2, keys, '', 'bech32')['address'])
|
||||
assert_equal(legacy_addr, node0.addmultisigaddress(2, keys, '', 'p2sh-segwit')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.createmultisig(2, keys, 'bech32')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.createmultisig(2, keys, 'p2sh-segwit')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'bech32')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'p2sh-segwit')['address'])
|
||||
|
||||
self.log.info('Testing sortedmulti descriptors with BIP 67 test vectors')
|
||||
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f:
|
||||
|
@ -89,6 +97,8 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
assert_equal(self.nodes[0].deriveaddresses(sorted_key_desc)[0], t['address'])
|
||||
|
||||
def check_addmultisigaddress_errors(self):
|
||||
if self.options.descriptors:
|
||||
return
|
||||
self.log.info('Check that addmultisigaddress fails when the private keys are missing')
|
||||
addresses = [self.nodes[1].getnewaddress(address_type='legacy') for _ in range(2)]
|
||||
assert_raises_rpc_error(-5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses))
|
||||
|
@ -115,6 +125,15 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
|
||||
def do_multisig(self):
|
||||
node0, node1, node2 = self.nodes
|
||||
if 'wmulti' not in node1.listwallets():
|
||||
try:
|
||||
node1.loadwallet('wmulti')
|
||||
except JSONRPCException as e:
|
||||
if e.error['code'] == -18 and 'Wallet wmulti not found' in e.error['message']:
|
||||
node1.createwallet(wallet_name='wmulti', disable_private_keys=True)
|
||||
else:
|
||||
raise
|
||||
wmulti = node1.get_wallet_rpc('wmulti')
|
||||
|
||||
# Construct the expected descriptor
|
||||
desc = 'multi({},{})'.format(self.nsigs, ','.join(self.pub))
|
||||
|
@ -134,7 +153,7 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
assert madd[0:4] == "bcrt" # actually a bech32 address
|
||||
|
||||
# compare against addmultisigaddress
|
||||
msigw = node1.addmultisigaddress(self.nsigs, self.pub, None, self.output_type)
|
||||
msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None, self.output_type)
|
||||
maddw = msigw["address"]
|
||||
mredeemw = msigw["redeemScript"]
|
||||
assert_equal(desc, drop_origins(msigw['descriptor']))
|
||||
|
@ -194,6 +213,8 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
|||
txinfo = node0.getrawtransaction(tx, True, blk)
|
||||
self.log.info("n/m=%d/%d %s size=%d vsize=%d weight=%d" % (self.nsigs, self.nkeys, self.output_type, txinfo["size"], txinfo["vsize"], txinfo["weight"]))
|
||||
|
||||
wmulti.unloadwallet()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
RpcCreateMultiSigTest().main()
|
||||
|
|
|
@ -48,18 +48,23 @@ class PSBTTest(BitcoinTestFramework):
|
|||
disconnect_nodes(offline_node, 2)
|
||||
disconnect_nodes(mining_node, 0)
|
||||
|
||||
# Create watchonly on online_node
|
||||
online_node.createwallet(wallet_name='wonline', disable_private_keys=True)
|
||||
wonline = online_node.get_wallet_rpc('wonline')
|
||||
w2 = online_node.get_wallet_rpc('')
|
||||
|
||||
# Mine a transaction that credits the offline address
|
||||
offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit")
|
||||
online_addr = online_node.getnewaddress(address_type="p2sh-segwit")
|
||||
online_node.importaddress(offline_addr, "", False)
|
||||
online_addr = w2.getnewaddress(address_type="p2sh-segwit")
|
||||
wonline.importaddress(offline_addr, "", False)
|
||||
mining_node.sendtoaddress(address=offline_addr, amount=1.0)
|
||||
mining_node.generate(nblocks=1)
|
||||
self.sync_blocks([mining_node, online_node])
|
||||
|
||||
# Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO)
|
||||
utxos = online_node.listunspent(addresses=[offline_addr])
|
||||
raw = online_node.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}])
|
||||
psbt = online_node.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"]
|
||||
utxos = wonline.listunspent(addresses=[offline_addr])
|
||||
raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}])
|
||||
psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"]
|
||||
assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0]
|
||||
|
||||
# Have the offline node sign the PSBT (which will update the UTXO to segwit)
|
||||
|
@ -72,6 +77,8 @@ class PSBTTest(BitcoinTestFramework):
|
|||
self.sync_blocks([mining_node, online_node])
|
||||
assert_equal(online_node.gettxout(txid,0)["confirmations"], 1)
|
||||
|
||||
wonline.unloadwallet()
|
||||
|
||||
# Reconnect
|
||||
connect_nodes(self.nodes[0], 1)
|
||||
connect_nodes(self.nodes[0], 2)
|
||||
|
@ -89,13 +96,23 @@ class PSBTTest(BitcoinTestFramework):
|
|||
final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex']
|
||||
self.nodes[0].sendrawtransaction(final_tx)
|
||||
|
||||
# Create p2sh, p2wpkh, and p2wsh addresses
|
||||
# Get pubkeys
|
||||
pubkey0 = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress())['pubkey']
|
||||
pubkey1 = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey']
|
||||
pubkey2 = self.nodes[2].getaddressinfo(self.nodes[2].getnewaddress())['pubkey']
|
||||
p2sh = self.nodes[1].addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "legacy")['address']
|
||||
p2wsh = self.nodes[1].addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "bech32")['address']
|
||||
p2sh_p2wsh = self.nodes[1].addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "p2sh-segwit")['address']
|
||||
|
||||
# Setup watchonly wallets
|
||||
self.nodes[2].createwallet(wallet_name='wmulti', disable_private_keys=True)
|
||||
wmulti = self.nodes[2].get_wallet_rpc('wmulti')
|
||||
|
||||
# Create all the addresses
|
||||
p2sh = wmulti.addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "legacy")['address']
|
||||
p2wsh = wmulti.addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "bech32")['address']
|
||||
p2sh_p2wsh = wmulti.addmultisigaddress(2, [pubkey0, pubkey1, pubkey2], "", "p2sh-segwit")['address']
|
||||
if not self.options.descriptors:
|
||||
wmulti.importaddress(p2sh)
|
||||
wmulti.importaddress(p2wsh)
|
||||
wmulti.importaddress(p2sh_p2wsh)
|
||||
p2wpkh = self.nodes[1].getnewaddress("", "bech32")
|
||||
p2pkh = self.nodes[1].getnewaddress("", "legacy")
|
||||
p2sh_p2wpkh = self.nodes[1].getnewaddress("", "p2sh-segwit")
|
||||
|
@ -146,11 +163,14 @@ class PSBTTest(BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10})
|
||||
|
||||
# partially sign multisig things with node 1
|
||||
psbtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], {self.nodes[1].getnewaddress():29.99})['psbt']
|
||||
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs={self.nodes[1].getnewaddress():29.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt']
|
||||
walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx)
|
||||
psbtx = walletprocesspsbt_out['psbt']
|
||||
assert_equal(walletprocesspsbt_out['complete'], False)
|
||||
|
||||
# Unload wmulti, we don't need it anymore
|
||||
wmulti.unloadwallet()
|
||||
|
||||
# partially sign with node 2. This should be complete and sendable
|
||||
walletprocesspsbt_out = self.nodes[2].walletprocesspsbt(psbtx)
|
||||
assert_equal(walletprocesspsbt_out['complete'], True)
|
||||
|
@ -297,7 +317,7 @@ class PSBTTest(BitcoinTestFramework):
|
|||
|
||||
# Signer tests
|
||||
for i, signer in enumerate(signers):
|
||||
self.nodes[2].createwallet("wallet{}".format(i))
|
||||
self.nodes[2].createwallet(wallet_name="wallet{}".format(i))
|
||||
wrpc = self.nodes[2].get_wallet_rpc("wallet{}".format(i))
|
||||
for key in signer['privkeys']:
|
||||
wrpc.importprivkey(key)
|
||||
|
|
|
@ -8,6 +8,8 @@ keys, and is trivially vulnerable to side channel attacks. Do not use for
|
|||
anything but tests."""
|
||||
import random
|
||||
|
||||
from .address import byte_to_base58
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
|
@ -384,3 +386,14 @@ class ECKey():
|
|||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
||||
|
||||
def bytes_to_wif(b, compressed=True):
|
||||
if compressed:
|
||||
b += b'\x01'
|
||||
return byte_to_base58(b, 239)
|
||||
|
||||
def generate_wif_key():
|
||||
# Makes a WIF privkey for imports
|
||||
k = ECKey()
|
||||
k.generate()
|
||||
return bytes_to_wif(k.get_bytes(), k.is_compressed)
|
||||
|
|
|
@ -165,6 +165,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required")
|
||||
parser.add_argument("--randomseed", type=int,
|
||||
help="set a random seed for deterministically reproducing a previous test run")
|
||||
parser.add_argument("--descriptors", default=False, action="store_true",
|
||||
help="Run test using a descriptor wallet")
|
||||
self.add_options(parser)
|
||||
self.options = parser.parse_args()
|
||||
|
||||
|
@ -333,11 +335,23 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
|
||||
def setup_nodes(self):
|
||||
"""Override this method to customize test node setup"""
|
||||
extra_args = None
|
||||
extra_args = [[]] * self.num_nodes
|
||||
wallets = [[]] * self.num_nodes
|
||||
if hasattr(self, "extra_args"):
|
||||
extra_args = self.extra_args
|
||||
wallets = [[x for x in eargs if x.startswith('-wallet=')] for eargs in extra_args]
|
||||
extra_args = [x + ['-nowallet'] for x in extra_args]
|
||||
self.add_nodes(self.num_nodes, extra_args)
|
||||
self.start_nodes()
|
||||
for i, n in enumerate(self.nodes):
|
||||
n.extra_args.pop()
|
||||
if '-wallet=0' in n.extra_args or '-nowallet' in n.extra_args or '-disablewallet' in n.extra_args or not self.is_wallet_compiled():
|
||||
continue
|
||||
if '-wallet=' not in wallets[i] and not any([x.startswith('-wallet=') for x in wallets[i]]):
|
||||
wallets[i].append('-wallet=')
|
||||
for w in wallets[i]:
|
||||
wallet_name = w.split('=', 1)[1]
|
||||
n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors)
|
||||
self.import_deterministic_coinbase_privkeys()
|
||||
if not self.setup_clean_chain:
|
||||
for n in self.nodes:
|
||||
|
@ -408,6 +422,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
use_cli=self.options.usecli,
|
||||
start_perf=self.options.perf,
|
||||
use_valgrind=self.options.valgrind,
|
||||
descriptors=self.options.descriptors,
|
||||
))
|
||||
|
||||
def start_node(self, i, *args, **kwargs):
|
||||
|
@ -547,6 +562,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
bitcoin_cli=self.options.bitcoincli,
|
||||
coverage_dir=None,
|
||||
cwd=self.options.tmpdir,
|
||||
descriptors=self.options.descriptors,
|
||||
))
|
||||
self.start_node(CACHE_NODE_ID)
|
||||
cache_node = self.nodes[CACHE_NODE_ID]
|
||||
|
|
|
@ -22,6 +22,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
from .authproxy import JSONRPCException
|
||||
from .descriptors import descsum_create
|
||||
from .util import (
|
||||
MAX_NODES,
|
||||
append_config,
|
||||
|
@ -61,7 +62,7 @@ class TestNode():
|
|||
To make things easier for the test writer, any unrecognised messages will
|
||||
be dispatched to the RPC connection."""
|
||||
|
||||
def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None):
|
||||
def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False):
|
||||
"""
|
||||
Kwargs:
|
||||
start_perf (bool): If True, begin profiling the node with `perf` as soon as
|
||||
|
@ -79,6 +80,7 @@ class TestNode():
|
|||
self.binary = bitcoind
|
||||
self.coverage_dir = coverage_dir
|
||||
self.cwd = cwd
|
||||
self.descriptors = descriptors
|
||||
if extra_conf is not None:
|
||||
append_config(datadir, extra_conf)
|
||||
# Most callers will just need to add extra args to the standard list below.
|
||||
|
@ -170,10 +172,10 @@ class TestNode():
|
|||
def __getattr__(self, name):
|
||||
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
|
||||
if self.use_cli:
|
||||
return getattr(self.cli, name)
|
||||
return getattr(RPCOverloadWrapper(self.cli, True, self.descriptors), name)
|
||||
else:
|
||||
assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection")
|
||||
return getattr(self.rpc, name)
|
||||
return getattr(RPCOverloadWrapper(self.rpc, descriptors=self.descriptors), name)
|
||||
|
||||
def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs):
|
||||
"""Start the node."""
|
||||
|
@ -265,11 +267,11 @@ class TestNode():
|
|||
|
||||
def get_wallet_rpc(self, wallet_name):
|
||||
if self.use_cli:
|
||||
return self.cli("-rpcwallet={}".format(wallet_name))
|
||||
return RPCOverloadWrapper(self.cli("-rpcwallet={}".format(wallet_name)), True, self.descriptors)
|
||||
else:
|
||||
assert self.rpc_connected and self.rpc, self._node_msg("RPC not connected")
|
||||
wallet_path = "wallet/{}".format(urllib.parse.quote(wallet_name))
|
||||
return self.rpc / wallet_path
|
||||
return RPCOverloadWrapper(self.rpc / wallet_path, descriptors=self.descriptors)
|
||||
|
||||
def stop_node(self, expected_stderr='', wait=0):
|
||||
"""Stop the node."""
|
||||
|
@ -595,3 +597,118 @@ class TestNodeCLI():
|
|||
return json.loads(cli_stdout, parse_float=decimal.Decimal)
|
||||
except json.JSONDecodeError:
|
||||
return cli_stdout.rstrip("\n")
|
||||
|
||||
class RPCOverloadWrapper():
|
||||
def __init__(self, rpc, cli=False, descriptors=False):
|
||||
self.rpc = rpc
|
||||
self.is_cli = cli
|
||||
self.descriptors = descriptors
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.rpc, name)
|
||||
|
||||
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase=None, avoid_reuse=None, descriptors=None):
|
||||
if self.is_cli:
|
||||
if disable_private_keys is None:
|
||||
disable_private_keys = 'null'
|
||||
if blank is None:
|
||||
blank = 'null'
|
||||
if passphrase is None:
|
||||
passphrase = ''
|
||||
if avoid_reuse is None:
|
||||
avoid_reuse = 'null'
|
||||
if descriptors is None:
|
||||
descriptors = self.descriptors
|
||||
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors)
|
||||
|
||||
def importprivkey(self, privkey, label=None, rescan=None):
|
||||
wallet_info = self.getwalletinfo()
|
||||
if self.is_cli:
|
||||
if label is None:
|
||||
label = 'null'
|
||||
if rescan is None:
|
||||
rescan = 'null'
|
||||
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
|
||||
return self.__getattr__('importprivkey')(privkey, label, rescan)
|
||||
desc = descsum_create('combo(' + privkey + ')')
|
||||
req = [{
|
||||
'desc': desc,
|
||||
'timestamp': 0 if rescan else 'now',
|
||||
'label': label if label else ''
|
||||
}]
|
||||
import_res = self.importdescriptors(req)
|
||||
if not import_res[0]['success']:
|
||||
raise JSONRPCException(import_res[0]['error'])
|
||||
|
||||
def addmultisigaddress(self, nrequired, keys, label=None, address_type=None):
|
||||
wallet_info = self.getwalletinfo()
|
||||
if self.is_cli:
|
||||
if label is None:
|
||||
label = 'null'
|
||||
if address_type is None:
|
||||
address_type = 'null'
|
||||
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
|
||||
return self.__getattr__('addmultisigaddress')(nrequired, keys, label, address_type)
|
||||
cms = self.createmultisig(nrequired, keys, address_type)
|
||||
req = [{
|
||||
'desc': cms['descriptor'],
|
||||
'timestamp': 0,
|
||||
'label': label if label else ''
|
||||
}]
|
||||
import_res = self.importdescriptors(req)
|
||||
if not import_res[0]['success']:
|
||||
raise JSONRPCException(import_res[0]['error'])
|
||||
return cms
|
||||
|
||||
def importpubkey(self, pubkey, label=None, rescan=None):
|
||||
wallet_info = self.getwalletinfo()
|
||||
if self.is_cli:
|
||||
if label is None:
|
||||
label = 'null'
|
||||
if rescan is None:
|
||||
rescan = 'null'
|
||||
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
|
||||
return self.__getattr__('importpubkey')(pubkey, label, rescan)
|
||||
desc = descsum_create('combo(' + pubkey + ')')
|
||||
req = [{
|
||||
'desc': desc,
|
||||
'timestamp': 0 if rescan else 'now',
|
||||
'label': label if label else ''
|
||||
}]
|
||||
import_res = self.importdescriptors(req)
|
||||
if not import_res[0]['success']:
|
||||
raise JSONRPCException(import_res[0]['error'])
|
||||
|
||||
def importaddress(self, address, label=None, rescan=None, p2sh=None):
|
||||
wallet_info = self.getwalletinfo()
|
||||
if self.is_cli:
|
||||
if label is None:
|
||||
label = 'null'
|
||||
if rescan is None:
|
||||
rescan = 'null'
|
||||
if p2sh is None:
|
||||
p2sh = 'null'
|
||||
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
|
||||
return self.__getattr__('importaddress')(address, label, rescan, p2sh)
|
||||
is_hex = False
|
||||
try:
|
||||
int(address ,16)
|
||||
is_hex = True
|
||||
desc = descsum_create('raw(' + address + ')')
|
||||
except:
|
||||
desc = descsum_create('addr(' + address + ')')
|
||||
reqs = [{
|
||||
'desc': desc,
|
||||
'timestamp': 0 if rescan else 'now',
|
||||
'label': label if label else ''
|
||||
}]
|
||||
if is_hex and p2sh:
|
||||
reqs.append({
|
||||
'desc': descsum_create('p2sh(raw(' + address + '))'),
|
||||
'timestamp': 0 if rescan else 'now',
|
||||
'label': label if label else ''
|
||||
})
|
||||
import_res = self.importdescriptors(reqs)
|
||||
for res in import_res:
|
||||
if not res['success']:
|
||||
raise JSONRPCException(res['error'])
|
||||
|
|
|
@ -13,6 +13,10 @@ from test_framework.address import (
|
|||
script_to_p2sh_p2wsh,
|
||||
script_to_p2wsh,
|
||||
)
|
||||
from test_framework.key import (
|
||||
bytes_to_wif,
|
||||
ECKey,
|
||||
)
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
|
@ -66,6 +70,25 @@ def get_key(node):
|
|||
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
|
||||
p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey))
|
||||
|
||||
def get_generate_key():
|
||||
"""Generate a fresh key
|
||||
|
||||
Returns a named tuple of privkey, pubkey and all address and scripts."""
|
||||
eckey = ECKey()
|
||||
eckey.generate()
|
||||
privkey = bytes_to_wif(eckey.get_bytes())
|
||||
pubkey = eckey.get_pubkey().get_bytes().hex()
|
||||
pkh = hash160(hex_str_to_bytes(pubkey))
|
||||
return Key(privkey=privkey,
|
||||
pubkey=pubkey,
|
||||
p2pkh_script=CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]).hex(),
|
||||
p2pkh_addr=key_to_p2pkh(pubkey),
|
||||
p2wpkh_script=CScript([OP_0, pkh]).hex(),
|
||||
p2wpkh_addr=key_to_p2wpkh(pubkey),
|
||||
p2sh_p2wpkh_script=CScript([OP_HASH160, hash160(CScript([OP_0, pkh])), OP_EQUAL]).hex(),
|
||||
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
|
||||
p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey))
|
||||
|
||||
def get_multisig(node):
|
||||
"""Generate a fresh 2-of-3 multisig on node
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ BASE_SCRIPTS = [
|
|||
# Scripts that are run by default.
|
||||
# Longest test should go first, to favor running tests in parallel
|
||||
'wallet_hd.py',
|
||||
'wallet_hd.py --descriptors',
|
||||
'wallet_backup.py',
|
||||
# vv Tests less than 5m vv
|
||||
'mining_getblocktemplate_longpoll.py',
|
||||
|
@ -86,7 +87,9 @@ BASE_SCRIPTS = [
|
|||
'feature_segwit.py',
|
||||
# vv Tests less than 2m vv
|
||||
'wallet_basic.py',
|
||||
'wallet_basic.py --descriptors',
|
||||
'wallet_labels.py',
|
||||
'wallet_labels.py --descriptors',
|
||||
'p2p_segwit.py',
|
||||
'p2p_timeouts.py',
|
||||
'p2p_tx_download.py',
|
||||
|
@ -109,6 +112,7 @@ BASE_SCRIPTS = [
|
|||
'feature_abortnode.py',
|
||||
# vv Tests less than 30s vv
|
||||
'wallet_keypool_topup.py',
|
||||
'wallet_keypool_topup.py --descriptors',
|
||||
'feature_fee_estimation.py',
|
||||
'interface_zmq.py',
|
||||
'interface_bitcoin_cli.py',
|
||||
|
@ -122,6 +126,7 @@ BASE_SCRIPTS = [
|
|||
'interface_rest.py',
|
||||
'mempool_spend_coinbase.py',
|
||||
'wallet_avoidreuse.py',
|
||||
'wallet_avoidreuse.py --descriptors',
|
||||
'mempool_reorg.py',
|
||||
'mempool_persist.py',
|
||||
'wallet_multiwallet.py',
|
||||
|
@ -134,6 +139,7 @@ BASE_SCRIPTS = [
|
|||
'interface_http.py',
|
||||
'interface_rpc.py',
|
||||
'rpc_psbt.py',
|
||||
'rpc_psbt.py --descriptors',
|
||||
'rpc_users.py',
|
||||
'rpc_whitelist.py',
|
||||
'feature_proxy.py',
|
||||
|
@ -147,6 +153,8 @@ BASE_SCRIPTS = [
|
|||
'p2p_addr_relay.py',
|
||||
'rpc_net.py',
|
||||
'wallet_keypool.py',
|
||||
'wallet_keypool.py --descriptors',
|
||||
'wallet_descriptor.py',
|
||||
'p2p_mempool.py',
|
||||
'p2p_filter.py',
|
||||
'rpc_setban.py',
|
||||
|
@ -168,6 +176,7 @@ BASE_SCRIPTS = [
|
|||
'mempool_packages.py',
|
||||
'mempool_package_onemore.py',
|
||||
'rpc_createmultisig.py',
|
||||
'rpc_createmultisig.py --descriptors',
|
||||
'feature_versionbits_warning.py',
|
||||
'rpc_preciousblock.py',
|
||||
'wallet_importprunedfunds.py',
|
||||
|
@ -180,6 +189,7 @@ BASE_SCRIPTS = [
|
|||
'mempool_expiry.py',
|
||||
'wallet_import_rescan.py',
|
||||
'wallet_import_with_label.py',
|
||||
'wallet_importdescriptors.py',
|
||||
'rpc_bind.py --ipv4',
|
||||
'rpc_bind.py --ipv6',
|
||||
'rpc_bind.py --nonloopback',
|
||||
|
@ -190,6 +200,7 @@ BASE_SCRIPTS = [
|
|||
'wallet_listsinceblock.py',
|
||||
'p2p_leak.py',
|
||||
'wallet_encryption.py',
|
||||
'wallet_encryption.py --descriptors',
|
||||
'feature_dersig.py',
|
||||
'feature_cltv.py',
|
||||
'rpc_uptime.py',
|
||||
|
|
|
@ -133,7 +133,7 @@ class AvoidReuseTest(BitcoinTestFramework):
|
|||
tempwallet = ".wallet_avoidreuse.py_test_immutable_wallet.dat"
|
||||
|
||||
# Create a wallet with disable_private_keys set; this should work
|
||||
self.nodes[1].createwallet(tempwallet, True)
|
||||
self.nodes[1].createwallet(wallet_name=tempwallet, disable_private_keys=True)
|
||||
w = self.nodes[1].get_wallet_rpc(tempwallet)
|
||||
|
||||
# Attempt to unset the disable_private_keys flag; this should not work
|
||||
|
@ -249,43 +249,44 @@ class AvoidReuseTest(BitcoinTestFramework):
|
|||
# getbalances should show no used, 5 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5})
|
||||
|
||||
# For the second send, we transmute it to a related single-key address
|
||||
# to make sure it's also detected as re-use
|
||||
fund_spk = self.nodes[0].getaddressinfo(fundaddr)["scriptPubKey"]
|
||||
fund_decoded = self.nodes[0].decodescript(fund_spk)
|
||||
if second_addr_type == "p2sh-segwit":
|
||||
new_fundaddr = fund_decoded["segwit"]["p2sh-segwit"]
|
||||
elif second_addr_type == "bech32":
|
||||
new_fundaddr = fund_decoded["segwit"]["addresses"][0]
|
||||
else:
|
||||
new_fundaddr = fundaddr
|
||||
assert_equal(second_addr_type, "legacy")
|
||||
if not self.options.descriptors:
|
||||
# For the second send, we transmute it to a related single-key address
|
||||
# to make sure it's also detected as re-use
|
||||
fund_spk = self.nodes[0].getaddressinfo(fundaddr)["scriptPubKey"]
|
||||
fund_decoded = self.nodes[0].decodescript(fund_spk)
|
||||
if second_addr_type == "p2sh-segwit":
|
||||
new_fundaddr = fund_decoded["segwit"]["p2sh-segwit"]
|
||||
elif second_addr_type == "bech32":
|
||||
new_fundaddr = fund_decoded["segwit"]["addresses"][0]
|
||||
else:
|
||||
new_fundaddr = fundaddr
|
||||
assert_equal(second_addr_type, "legacy")
|
||||
|
||||
self.nodes[0].sendtoaddress(new_fundaddr, 10)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
self.nodes[0].sendtoaddress(new_fundaddr, 10)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10)
|
||||
assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10)
|
||||
# getbalances should show 10 used, 5 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 10, "trusted": 5})
|
||||
# listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10)
|
||||
assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10)
|
||||
# getbalances should show 10 used, 5 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 10, "trusted": 5})
|
||||
|
||||
# node 1 should now have a balance of 5 (no dirty) or 15 (including dirty)
|
||||
assert_approx(self.nodes[1].getbalance(), 5, 0.001)
|
||||
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 15, 0.001)
|
||||
# node 1 should now have a balance of 5 (no dirty) or 15 (including dirty)
|
||||
assert_approx(self.nodes[1].getbalance(), 5, 0.001)
|
||||
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 15, 0.001)
|
||||
|
||||
assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10)
|
||||
assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10)
|
||||
|
||||
self.nodes[1].sendtoaddress(retaddr, 4)
|
||||
self.nodes[1].sendtoaddress(retaddr, 4)
|
||||
|
||||
# listunspent should show 2 total outputs (1, 10 btc), one unused (1), one reused (10)
|
||||
assert_unspent(self.nodes[1], total_count=2, total_sum=11, reused_count=1, reused_sum=10)
|
||||
# getbalances should show 10 used, 1 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 10, "trusted": 1})
|
||||
# listunspent should show 2 total outputs (1, 10 btc), one unused (1), one reused (10)
|
||||
assert_unspent(self.nodes[1], total_count=2, total_sum=11, reused_count=1, reused_sum=10)
|
||||
# getbalances should show 10 used, 1 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 10, "trusted": 1})
|
||||
|
||||
# node 1 should now have about 1 btc left (no dirty) and 11 (including dirty)
|
||||
assert_approx(self.nodes[1].getbalance(), 1, 0.001)
|
||||
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001)
|
||||
# node 1 should now have about 1 btc left (no dirty) and 11 (including dirty)
|
||||
assert_approx(self.nodes[1].getbalance(), 1, 0.001)
|
||||
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001)
|
||||
|
||||
def test_getbalances_used(self):
|
||||
'''
|
||||
|
|
|
@ -49,6 +49,7 @@ class WalletTest(BitcoinTestFramework):
|
|||
return self.nodes[0].decoderawtransaction(txn)['vsize']
|
||||
|
||||
def run_test(self):
|
||||
|
||||
# Check that there's no UTXO on none of the nodes
|
||||
assert_equal(len(self.nodes[0].listunspent()), 0)
|
||||
assert_equal(len(self.nodes[1].listunspent()), 0)
|
||||
|
@ -219,7 +220,7 @@ class WalletTest(BitcoinTestFramework):
|
|||
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, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
|
||||
|
||||
self.start_node(3)
|
||||
self.start_node(3, self.nodes[3].extra_args)
|
||||
connect_nodes(self.nodes[0], 3)
|
||||
self.sync_all()
|
||||
|
||||
|
@ -315,57 +316,59 @@ class WalletTest(BitcoinTestFramework):
|
|||
# This will raise an exception since generate does not accept a string
|
||||
assert_raises_rpc_error(-1, "not an integer", self.nodes[0].generate, "2")
|
||||
|
||||
# This will raise an exception for the invalid private key format
|
||||
assert_raises_rpc_error(-5, "Invalid private key encoding", self.nodes[0].importprivkey, "invalid")
|
||||
if not self.options.descriptors:
|
||||
|
||||
# This will raise an exception for importing an address with the PS2H flag
|
||||
temp_address = self.nodes[1].getnewaddress("", "p2sh-segwit")
|
||||
assert_raises_rpc_error(-5, "Cannot use the p2sh flag with an address - use a script instead", self.nodes[0].importaddress, temp_address, "label", False, True)
|
||||
# This will raise an exception for the invalid private key format
|
||||
assert_raises_rpc_error(-5, "Invalid private key encoding", self.nodes[0].importprivkey, "invalid")
|
||||
|
||||
# This will raise an exception for attempting to dump the private key of an address you do not own
|
||||
assert_raises_rpc_error(-3, "Address does not refer to a key", self.nodes[0].dumpprivkey, temp_address)
|
||||
# This will raise an exception for importing an address with the PS2H flag
|
||||
temp_address = self.nodes[1].getnewaddress("", "p2sh-segwit")
|
||||
assert_raises_rpc_error(-5, "Cannot use the p2sh flag with an address - use a script instead", self.nodes[0].importaddress, temp_address, "label", False, True)
|
||||
|
||||
# This will raise an exception for attempting to get the private key of an invalid Bitcoin address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].dumpprivkey, "invalid")
|
||||
# This will raise an exception for attempting to dump the private key of an address you do not own
|
||||
assert_raises_rpc_error(-3, "Address does not refer to a key", self.nodes[0].dumpprivkey, temp_address)
|
||||
|
||||
# This will raise an exception for attempting to set a label for an invalid Bitcoin address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].setlabel, "invalid address", "label")
|
||||
# This will raise an exception for attempting to get the private key of an invalid Bitcoin address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].dumpprivkey, "invalid")
|
||||
|
||||
# This will raise an exception for importing an invalid address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address or script", self.nodes[0].importaddress, "invalid")
|
||||
# This will raise an exception for attempting to set a label for an invalid Bitcoin address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].setlabel, "invalid address", "label")
|
||||
|
||||
# This will raise an exception for attempting to import a pubkey that isn't in hex
|
||||
assert_raises_rpc_error(-5, "Pubkey must be a hex string", self.nodes[0].importpubkey, "not hex")
|
||||
# This will raise an exception for importing an invalid address
|
||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address or script", self.nodes[0].importaddress, "invalid")
|
||||
|
||||
# This will raise an exception for importing an invalid pubkey
|
||||
assert_raises_rpc_error(-5, "Pubkey is not a valid public key", self.nodes[0].importpubkey, "5361746f736869204e616b616d6f746f")
|
||||
# This will raise an exception for attempting to import a pubkey that isn't in hex
|
||||
assert_raises_rpc_error(-5, "Pubkey must be a hex string", self.nodes[0].importpubkey, "not hex")
|
||||
|
||||
# Import address and private key to check correct behavior of spendable unspents
|
||||
# 1. Send some coins to generate new UTXO
|
||||
address_to_import = self.nodes[2].getnewaddress()
|
||||
txid = self.nodes[0].sendtoaddress(address_to_import, 1)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
# This will raise an exception for importing an invalid pubkey
|
||||
assert_raises_rpc_error(-5, "Pubkey is not a valid public key", self.nodes[0].importpubkey, "5361746f736869204e616b616d6f746f")
|
||||
|
||||
# 2. Import address from node2 to node1
|
||||
self.nodes[1].importaddress(address_to_import)
|
||||
# Import address and private key to check correct behavior of spendable unspents
|
||||
# 1. Send some coins to generate new UTXO
|
||||
address_to_import = self.nodes[2].getnewaddress()
|
||||
txid = self.nodes[0].sendtoaddress(address_to_import, 1)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
|
||||
# 3. Validate that the imported address is watch-only on node1
|
||||
assert self.nodes[1].getaddressinfo(address_to_import)["iswatchonly"]
|
||||
# 2. Import address from node2 to node1
|
||||
self.nodes[1].importaddress(address_to_import)
|
||||
|
||||
# 4. Check that the unspents after import are not spendable
|
||||
assert_array_result(self.nodes[1].listunspent(),
|
||||
{"address": address_to_import},
|
||||
{"spendable": False})
|
||||
# 3. Validate that the imported address is watch-only on node1
|
||||
assert self.nodes[1].getaddressinfo(address_to_import)["iswatchonly"]
|
||||
|
||||
# 5. Import private key of the previously imported address on node1
|
||||
priv_key = self.nodes[2].dumpprivkey(address_to_import)
|
||||
self.nodes[1].importprivkey(priv_key)
|
||||
# 4. Check that the unspents after import are not spendable
|
||||
assert_array_result(self.nodes[1].listunspent(),
|
||||
{"address": address_to_import},
|
||||
{"spendable": False})
|
||||
|
||||
# 6. Check that the unspents are now spendable on node1
|
||||
assert_array_result(self.nodes[1].listunspent(),
|
||||
{"address": address_to_import},
|
||||
{"spendable": True})
|
||||
# 5. Import private key of the previously imported address on node1
|
||||
priv_key = self.nodes[2].dumpprivkey(address_to_import)
|
||||
self.nodes[1].importprivkey(priv_key)
|
||||
|
||||
# 6. Check that the unspents are now spendable on node1
|
||||
assert_array_result(self.nodes[1].listunspent(),
|
||||
{"address": address_to_import},
|
||||
{"spendable": True})
|
||||
|
||||
# Mine a block from node0 to an address from node1
|
||||
coinbase_addr = self.nodes[1].getnewaddress()
|
||||
|
@ -460,7 +463,8 @@ class WalletTest(BitcoinTestFramework):
|
|||
# Try with walletrejectlongchains
|
||||
# Double chain limit but require combining inputs, so we pass SelectCoinsMinConf
|
||||
self.stop_node(0)
|
||||
self.start_node(0, extra_args=["-walletrejectlongchains", "-limitancestorcount=" + str(2 * chainlimit)])
|
||||
extra_args = ["-walletrejectlongchains", "-limitancestorcount=" + str(2 * chainlimit)]
|
||||
self.start_node(0, extra_args=extra_args)
|
||||
|
||||
# wait for loadmempool
|
||||
timeout = 10
|
||||
|
|
144
test/functional/wallet_descriptor.py
Executable file
144
test/functional/wallet_descriptor.py
Executable file
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2019 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test descriptor wallet function."""
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error
|
||||
)
|
||||
|
||||
|
||||
class WalletDescriptorTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 1
|
||||
self.extra_args = [['-keypool=100']]
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def run_test(self):
|
||||
# Make a descriptor wallet
|
||||
self.log.info("Making a descriptor wallet")
|
||||
self.nodes[0].createwallet(wallet_name="desc1", descriptors=True)
|
||||
self.nodes[0].unloadwallet("")
|
||||
|
||||
# A descriptor wallet should have 100 addresses * 3 types = 300 keys
|
||||
self.log.info("Checking wallet info")
|
||||
wallet_info = self.nodes[0].getwalletinfo()
|
||||
assert_equal(wallet_info['keypoolsize'], 300)
|
||||
assert_equal(wallet_info['keypoolsize_hd_internal'], 300)
|
||||
assert 'keypoololdest' not in wallet_info
|
||||
|
||||
# Check that getnewaddress works
|
||||
self.log.info("Test that getnewaddress and getrawchangeaddress work")
|
||||
addr = self.nodes[0].getnewaddress("", "legacy")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('pkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/44\'/1\'/0\'/0/0')
|
||||
|
||||
addr = self.nodes[0].getnewaddress("", "p2sh-segwit")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('sh(wpkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/49\'/1\'/0\'/0/0')
|
||||
|
||||
addr = self.nodes[0].getnewaddress("", "bech32")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('wpkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/84\'/1\'/0\'/0/0')
|
||||
|
||||
# Check that getrawchangeaddress works
|
||||
addr = self.nodes[0].getrawchangeaddress("legacy")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('pkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/44\'/1\'/0\'/1/0')
|
||||
|
||||
addr = self.nodes[0].getrawchangeaddress("p2sh-segwit")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('sh(wpkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/49\'/1\'/0\'/1/0')
|
||||
|
||||
addr = self.nodes[0].getrawchangeaddress("bech32")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('wpkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/84\'/1\'/0\'/1/0')
|
||||
|
||||
# Make a wallet to receive coins at
|
||||
self.nodes[0].createwallet(wallet_name="desc2", descriptors=True)
|
||||
recv_wrpc = self.nodes[0].get_wallet_rpc("desc2")
|
||||
send_wrpc = self.nodes[0].get_wallet_rpc("desc1")
|
||||
|
||||
# Generate some coins
|
||||
send_wrpc.generatetoaddress(101, send_wrpc.getnewaddress())
|
||||
|
||||
# Make transactions
|
||||
self.log.info("Test sending and receiving")
|
||||
addr = recv_wrpc.getnewaddress()
|
||||
send_wrpc.sendtoaddress(addr, 10)
|
||||
|
||||
# Make sure things are disabled
|
||||
self.log.info("Test disabled RPCs")
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.importprivkey, "cVpF924EspNh8KjYsfhgY96mmxvT6DgdWiTYMtMjuM74hJaU5psW")
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.importpubkey, send_wrpc.getaddressinfo(send_wrpc.getnewaddress()))
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.importaddress, recv_wrpc.getnewaddress())
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.importmulti, [])
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.addmultisigaddress, 1, [recv_wrpc.getnewaddress()])
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.dumpprivkey, recv_wrpc.getnewaddress())
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.dumpwallet, 'wallet.dump')
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.importwallet, 'wallet.dump')
|
||||
assert_raises_rpc_error(-4, "This type of wallet does not support this command", recv_wrpc.rpc.sethdseed)
|
||||
|
||||
self.log.info("Test encryption")
|
||||
# Get the master fingerprint before encrypt
|
||||
info1 = send_wrpc.getaddressinfo(send_wrpc.getnewaddress())
|
||||
|
||||
# Encrypt wallet 0
|
||||
send_wrpc.encryptwallet('pass')
|
||||
send_wrpc.walletpassphrase('pass', 10)
|
||||
addr = send_wrpc.getnewaddress()
|
||||
info2 = send_wrpc.getaddressinfo(addr)
|
||||
assert info1['hdmasterfingerprint'] != info2['hdmasterfingerprint']
|
||||
send_wrpc.walletlock()
|
||||
assert 'hdmasterfingerprint' in send_wrpc.getaddressinfo(send_wrpc.getnewaddress())
|
||||
info3 = send_wrpc.getaddressinfo(addr)
|
||||
assert_equal(info2['desc'], info3['desc'])
|
||||
|
||||
self.log.info("Test that getnewaddress still works after keypool is exhausted in an encrypted wallet")
|
||||
for i in range(0, 500):
|
||||
send_wrpc.getnewaddress()
|
||||
|
||||
self.log.info("Test that unlock is needed when deriving only hardened keys in an encrypted wallet")
|
||||
send_wrpc.walletpassphrase('pass', 10)
|
||||
send_wrpc.importdescriptors([{
|
||||
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
|
||||
"timestamp": "now",
|
||||
"range": [0,10],
|
||||
"active": True
|
||||
}])
|
||||
send_wrpc.walletlock()
|
||||
# Exhaust keypool of 100
|
||||
for i in range(0, 100):
|
||||
send_wrpc.getnewaddress(address_type='bech32')
|
||||
# This should now error
|
||||
assert_raises_rpc_error(-12, "Keypool ran out, please call keypoolrefill first", send_wrpc.getnewaddress, '', 'bech32')
|
||||
|
||||
self.log.info("Test born encrypted wallets")
|
||||
self.nodes[0].createwallet('desc_enc', False, False, 'pass', False, True)
|
||||
enc_rpc = self.nodes[0].get_wallet_rpc('desc_enc')
|
||||
enc_rpc.getnewaddress() # Makes sure that we can get a new address from a born encrypted wallet
|
||||
|
||||
self.log.info("Test blank descriptor wallets")
|
||||
self.nodes[0].createwallet(wallet_name='desc_blank', blank=True, descriptors=True)
|
||||
blank_rpc = self.nodes[0].get_wallet_rpc('desc_blank')
|
||||
assert_raises_rpc_error(-4, 'This wallet has no available keys', blank_rpc.getnewaddress)
|
||||
|
||||
self.log.info("Test descriptor wallet with disabled private keys")
|
||||
self.nodes[0].createwallet(wallet_name='desc_no_priv', disable_private_keys=True, descriptors=True)
|
||||
nopriv_rpc = self.nodes[0].get_wallet_rpc('desc_no_priv')
|
||||
assert_raises_rpc_error(-4, 'This wallet has no available keys', nopriv_rpc.getnewaddress)
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletDescriptorTest().main ()
|
|
@ -8,7 +8,6 @@ import time
|
|||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
assert_greater_than,
|
||||
assert_greater_than_or_equal,
|
||||
|
@ -27,10 +26,10 @@ class WalletEncryptionTest(BitcoinTestFramework):
|
|||
passphrase2 = "SecondWalletPassphrase"
|
||||
|
||||
# Make sure the wallet isn't encrypted first
|
||||
address = self.nodes[0].getnewaddress()
|
||||
privkey = self.nodes[0].dumpprivkey(address)
|
||||
assert_equal(privkey[:1], "c")
|
||||
assert_equal(len(privkey), 52)
|
||||
msg = "test message"
|
||||
address = self.nodes[0].getnewaddress(address_type='legacy')
|
||||
sig = self.nodes[0].signmessage(address, msg)
|
||||
assert self.nodes[0].verifymessage(address, sig, msg)
|
||||
assert_raises_rpc_error(-15, "Error: running with an unencrypted wallet, but walletpassphrase was called", self.nodes[0].walletpassphrase, 'ff', 1)
|
||||
assert_raises_rpc_error(-15, "Error: running with an unencrypted wallet, but walletpassphrasechange was called.", self.nodes[0].walletpassphrasechange, 'ff', 'ff')
|
||||
|
||||
|
@ -39,33 +38,36 @@ class WalletEncryptionTest(BitcoinTestFramework):
|
|||
self.nodes[0].encryptwallet(passphrase)
|
||||
|
||||
# Test that the wallet is encrypted
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].dumpprivkey, address)
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].signmessage, address, msg)
|
||||
assert_raises_rpc_error(-15, "Error: running with an encrypted wallet, but encryptwallet was called.", self.nodes[0].encryptwallet, 'ff')
|
||||
assert_raises_rpc_error(-8, "passphrase can not be empty", self.nodes[0].walletpassphrase, '', 1)
|
||||
assert_raises_rpc_error(-8, "passphrase can not be empty", self.nodes[0].walletpassphrasechange, '', 'ff')
|
||||
|
||||
# Check that walletpassphrase works
|
||||
self.nodes[0].walletpassphrase(passphrase, 2)
|
||||
assert_equal(privkey, self.nodes[0].dumpprivkey(address))
|
||||
sig = self.nodes[0].signmessage(address, msg)
|
||||
assert self.nodes[0].verifymessage(address, sig, msg)
|
||||
|
||||
# Check that the timeout is right
|
||||
time.sleep(3)
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].dumpprivkey, address)
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].signmessage, address, msg)
|
||||
|
||||
# Test wrong passphrase
|
||||
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase + "wrong", 10)
|
||||
|
||||
# Test walletlock
|
||||
self.nodes[0].walletpassphrase(passphrase, 84600)
|
||||
assert_equal(privkey, self.nodes[0].dumpprivkey(address))
|
||||
sig = self.nodes[0].signmessage(address, msg)
|
||||
assert self.nodes[0].verifymessage(address, sig, msg)
|
||||
self.nodes[0].walletlock()
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].dumpprivkey, address)
|
||||
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].signmessage, address, msg)
|
||||
|
||||
# Test passphrase changes
|
||||
self.nodes[0].walletpassphrasechange(passphrase, passphrase2)
|
||||
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase, 10)
|
||||
self.nodes[0].walletpassphrase(passphrase2, 10)
|
||||
assert_equal(privkey, self.nodes[0].dumpprivkey(address))
|
||||
sig = self.nodes[0].signmessage(address, msg)
|
||||
assert self.nodes[0].verifymessage(address, sig, msg)
|
||||
self.nodes[0].walletlock()
|
||||
|
||||
# Test timeout bounds
|
||||
|
|
|
@ -27,17 +27,21 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
|
||||
def run_test(self):
|
||||
# Make sure we use hd, keep masterkeyid
|
||||
masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert_equal(len(masterkeyid), 40)
|
||||
hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
|
||||
assert_equal(len(hd_fingerprint), 8)
|
||||
|
||||
# create an internal key
|
||||
change_addr = self.nodes[1].getrawchangeaddress()
|
||||
change_addrV= self.nodes[1].getaddressinfo(change_addr)
|
||||
assert_equal(change_addrV["hdkeypath"], "m/0'/1'/0'") #first internal child key
|
||||
if self.options.descriptors:
|
||||
assert_equal(change_addrV["hdkeypath"], "m/84'/1'/0'/1/0")
|
||||
else:
|
||||
assert_equal(change_addrV["hdkeypath"], "m/0'/1'/0'") #first internal child key
|
||||
|
||||
# Import a non-HD private key in the HD wallet
|
||||
non_hd_add = self.nodes[0].getnewaddress()
|
||||
self.nodes[1].importprivkey(self.nodes[0].dumpprivkey(non_hd_add))
|
||||
non_hd_add = 'bcrt1qmevj8zfx0wdvp05cqwkmr6mxkfx60yezwjksmt'
|
||||
non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt'
|
||||
self.nodes[1].importprivkey(non_hd_key)
|
||||
|
||||
# This should be enough to keep the master key and the non-HD key
|
||||
self.nodes[1].backupwallet(os.path.join(self.nodes[1].datadir, "hd.bak"))
|
||||
|
@ -48,11 +52,14 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
self.nodes[0].generate(101)
|
||||
hd_add = None
|
||||
NUM_HD_ADDS = 10
|
||||
for i in range(NUM_HD_ADDS):
|
||||
for i in range(1, NUM_HD_ADDS + 1):
|
||||
hd_add = self.nodes[1].getnewaddress()
|
||||
hd_info = self.nodes[1].getaddressinfo(hd_add)
|
||||
assert_equal(hd_info["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info["hdseedid"], masterkeyid)
|
||||
if self.options.descriptors:
|
||||
assert_equal(hd_info["hdkeypath"], "m/84'/1'/0'/0/" + str(i))
|
||||
else:
|
||||
assert_equal(hd_info["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint)
|
||||
self.nodes[0].sendtoaddress(hd_add, 1)
|
||||
self.nodes[0].generate(1)
|
||||
self.nodes[0].sendtoaddress(non_hd_add, 1)
|
||||
|
@ -61,7 +68,10 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
# create an internal key (again)
|
||||
change_addr = self.nodes[1].getrawchangeaddress()
|
||||
change_addrV= self.nodes[1].getaddressinfo(change_addr)
|
||||
assert_equal(change_addrV["hdkeypath"], "m/0'/1'/1'") #second internal child key
|
||||
if self.options.descriptors:
|
||||
assert_equal(change_addrV["hdkeypath"], "m/84'/1'/0'/1/1")
|
||||
else:
|
||||
assert_equal(change_addrV["hdkeypath"], "m/0'/1'/1'") #second internal child key
|
||||
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
|
||||
|
@ -72,16 +82,19 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
# otherwise node1 would auto-recover all funds in flag the keypool keys as used
|
||||
shutil.rmtree(os.path.join(self.nodes[1].datadir, self.chain, "blocks"))
|
||||
shutil.rmtree(os.path.join(self.nodes[1].datadir, self.chain, "chainstate"))
|
||||
shutil.copyfile(os.path.join(self.nodes[1].datadir, "hd.bak"), os.path.join(self.nodes[1].datadir, self.chain, "wallets", "wallet.dat"))
|
||||
shutil.copyfile(os.path.join(self.nodes[1].datadir, "hd.bak"), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', "wallet.dat"))
|
||||
self.start_node(1)
|
||||
|
||||
# Assert that derivation is deterministic
|
||||
hd_add_2 = None
|
||||
for i in range(NUM_HD_ADDS):
|
||||
for i in range(1, NUM_HD_ADDS + 1):
|
||||
hd_add_2 = self.nodes[1].getnewaddress()
|
||||
hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
|
||||
assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info_2["hdseedid"], masterkeyid)
|
||||
if self.options.descriptors:
|
||||
assert_equal(hd_info_2["hdkeypath"], "m/84'/1'/0'/0/" + str(i))
|
||||
else:
|
||||
assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
|
||||
assert_equal(hd_add, hd_add_2)
|
||||
connect_nodes(self.nodes[0], 1)
|
||||
self.sync_all()
|
||||
|
@ -117,41 +130,45 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
if out['value'] != 1:
|
||||
keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['addresses'][0])['hdkeypath']
|
||||
|
||||
assert_equal(keypath[0:7], "m/0'/1'")
|
||||
if self.options.descriptors:
|
||||
assert_equal(keypath[0:14], "m/84'/1'/0'/1/")
|
||||
else:
|
||||
assert_equal(keypath[0:7], "m/0'/1'")
|
||||
|
||||
# Generate a new HD seed on node 1 and make sure it is set
|
||||
orig_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
self.nodes[1].sethdseed()
|
||||
new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert orig_masterkeyid != new_masterkeyid
|
||||
addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/0\'') # Make sure the new address is the first from the keypool
|
||||
self.nodes[1].keypoolrefill(1) # Fill keypool with 1 key
|
||||
if not self.options.descriptors:
|
||||
# Generate a new HD seed on node 1 and make sure it is set
|
||||
orig_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
self.nodes[1].sethdseed()
|
||||
new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert orig_masterkeyid != new_masterkeyid
|
||||
addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/0\'') # Make sure the new address is the first from the keypool
|
||||
self.nodes[1].keypoolrefill(1) # Fill keypool with 1 key
|
||||
|
||||
# Set a new HD seed on node 1 without flushing the keypool
|
||||
new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
|
||||
orig_masterkeyid = new_masterkeyid
|
||||
self.nodes[1].sethdseed(False, new_seed)
|
||||
new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert orig_masterkeyid != new_masterkeyid
|
||||
addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo(addr)['hdseedid'])
|
||||
assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/1\'') # Make sure the new address continues previous keypool
|
||||
# Set a new HD seed on node 1 without flushing the keypool
|
||||
new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
|
||||
orig_masterkeyid = new_masterkeyid
|
||||
self.nodes[1].sethdseed(False, new_seed)
|
||||
new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert orig_masterkeyid != new_masterkeyid
|
||||
addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo(addr)['hdseedid'])
|
||||
assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/1\'') # Make sure the new address continues previous keypool
|
||||
|
||||
# Check that the next address is from the new seed
|
||||
self.nodes[1].keypoolrefill(1)
|
||||
next_addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo(next_addr)['hdseedid'])
|
||||
assert_equal(self.nodes[1].getaddressinfo(next_addr)['hdkeypath'], 'm/0\'/0\'/0\'') # Make sure the new address is not from previous keypool
|
||||
assert next_addr != addr
|
||||
# Check that the next address is from the new seed
|
||||
self.nodes[1].keypoolrefill(1)
|
||||
next_addr = self.nodes[1].getnewaddress()
|
||||
assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo(next_addr)['hdseedid'])
|
||||
assert_equal(self.nodes[1].getaddressinfo(next_addr)['hdkeypath'], 'm/0\'/0\'/0\'') # Make sure the new address is not from previous keypool
|
||||
assert next_addr != addr
|
||||
|
||||
# Sethdseed parameter validity
|
||||
assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0)
|
||||
assert_raises_rpc_error(-5, "Invalid private key", self.nodes[1].sethdseed, False, "not_wif")
|
||||
assert_raises_rpc_error(-1, "JSON value is not a boolean as expected", self.nodes[1].sethdseed, "Not_bool")
|
||||
assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[1].sethdseed, False, True)
|
||||
assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, new_seed)
|
||||
assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress()))
|
||||
# Sethdseed parameter validity
|
||||
assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0)
|
||||
assert_raises_rpc_error(-5, "Invalid private key", self.nodes[1].sethdseed, False, "not_wif")
|
||||
assert_raises_rpc_error(-1, "JSON value is not a boolean as expected", self.nodes[1].sethdseed, "Not_bool")
|
||||
assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[1].sethdseed, False, True)
|
||||
assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, new_seed)
|
||||
assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress()))
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletHDTest().main ()
|
||||
|
|
444
test/functional/wallet_importdescriptors.py
Executable file
444
test/functional/wallet_importdescriptors.py
Executable file
|
@ -0,0 +1,444 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2019 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test the importdescriptors RPC.
|
||||
|
||||
Test importdescriptors by generating keys on node0, importing the corresponding
|
||||
descriptors on node1 and then testing the address info for the different address
|
||||
variants.
|
||||
|
||||
- `get_generate_key()` is called to generate keys and return the privkeys,
|
||||
pubkeys and all variants of scriptPubKey and address.
|
||||
- `test_importdesc()` is called to send an importdescriptors call to node1, test
|
||||
success, and (if unsuccessful) test the error code and error message returned.
|
||||
- `test_address()` is called to call getaddressinfo for an address on node1
|
||||
and test the values returned."""
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
find_vout_for_address,
|
||||
)
|
||||
from test_framework.wallet_util import (
|
||||
get_generate_key,
|
||||
test_address,
|
||||
)
|
||||
|
||||
class ImportDescriptorsTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 2
|
||||
self.extra_args = [["-addresstype=legacy"],
|
||||
["-addresstype=bech32", "-keypool=5"]
|
||||
]
|
||||
self.setup_clean_chain = True
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def test_importdesc(self, req, success, error_code=None, error_message=None, warnings=None, wallet=None):
|
||||
"""Run importdescriptors and assert success"""
|
||||
if warnings is None:
|
||||
warnings = []
|
||||
wrpc = self.nodes[1].get_wallet_rpc('w1')
|
||||
if wallet is not None:
|
||||
wrpc = wallet
|
||||
|
||||
result = wrpc.importdescriptors([req])
|
||||
observed_warnings = []
|
||||
if 'warnings' in result[0]:
|
||||
observed_warnings = result[0]['warnings']
|
||||
assert_equal("\n".join(sorted(warnings)), "\n".join(sorted(observed_warnings)))
|
||||
assert_equal(result[0]['success'], success)
|
||||
if error_code is not None:
|
||||
assert_equal(result[0]['error']['code'], error_code)
|
||||
assert_equal(result[0]['error']['message'], error_message)
|
||||
|
||||
def run_test(self):
|
||||
self.log.info('Setting up wallets')
|
||||
self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)
|
||||
w0 = self.nodes[0].get_wallet_rpc('w0')
|
||||
|
||||
self.nodes[1].createwallet(wallet_name='w1', disable_private_keys=True, blank=True, descriptors=True)
|
||||
w1 = self.nodes[1].get_wallet_rpc('w1')
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
self.nodes[1].createwallet(wallet_name="wpriv", disable_private_keys=False, blank=True, descriptors=True)
|
||||
wpriv = self.nodes[1].get_wallet_rpc("wpriv")
|
||||
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
self.log.info('Mining coins')
|
||||
w0.generatetoaddress(101, w0.getnewaddress())
|
||||
|
||||
# RPC importdescriptors -----------------------------------------------
|
||||
|
||||
# # Test import fails if no descriptor present
|
||||
key = get_generate_key()
|
||||
self.log.info("Import should fail if a descriptor is not provided")
|
||||
self.test_importdesc({"timestamp": "now"},
|
||||
success=False,
|
||||
error_code=-8,
|
||||
error_message='Descriptor not found.')
|
||||
|
||||
# # Test importing of a P2PKH descriptor
|
||||
key = get_generate_key()
|
||||
self.log.info("Should import a p2pkh descriptor")
|
||||
self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"),
|
||||
"timestamp": "now",
|
||||
"label": "Descriptor import test"},
|
||||
success=True)
|
||||
test_address(w1,
|
||||
key.p2pkh_addr,
|
||||
solvable=True,
|
||||
ismine=True,
|
||||
labels=["Descriptor import test"])
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
self.log.info("Internal addresses cannot have labels")
|
||||
self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"),
|
||||
"timestamp": "now",
|
||||
"internal": True,
|
||||
"label": "Descriptor import test"},
|
||||
success=False,
|
||||
error_code=-8,
|
||||
error_message="Internal addresses should not have a label")
|
||||
|
||||
# # Test importing of a P2SH-P2WPKH descriptor
|
||||
key = get_generate_key()
|
||||
self.log.info("Should not import a p2sh-p2wpkh descriptor without checksum")
|
||||
self.test_importdesc({"desc": "sh(wpkh(" + key.pubkey + "))",
|
||||
"timestamp": "now"
|
||||
},
|
||||
success=False,
|
||||
error_code=-5,
|
||||
error_message="Missing checksum")
|
||||
|
||||
self.log.info("Should not import a p2sh-p2wpkh descriptor that has range specified")
|
||||
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||
"timestamp": "now",
|
||||
"range": 1,
|
||||
},
|
||||
success=False,
|
||||
error_code=-8,
|
||||
error_message="Range should not be specified for an un-ranged descriptor")
|
||||
|
||||
self.log.info("Should not import a p2sh-p2wpkh descriptor and have it set to active")
|
||||
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||
"timestamp": "now",
|
||||
"active": True,
|
||||
},
|
||||
success=False,
|
||||
error_code=-8,
|
||||
error_message="Active descriptors must be ranged")
|
||||
|
||||
self.log.info("Should import a (non-active) p2sh-p2wpkh descriptor")
|
||||
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||
"timestamp": "now",
|
||||
"active": False,
|
||||
},
|
||||
success=True)
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
test_address(w1,
|
||||
key.p2sh_p2wpkh_addr,
|
||||
ismine=True,
|
||||
solvable=True)
|
||||
|
||||
# # Test importing of a multisig descriptor
|
||||
key1 = get_generate_key()
|
||||
key2 = get_generate_key()
|
||||
self.log.info("Should import a 1-of-2 bare multisig from descriptor")
|
||||
self.test_importdesc({"desc": descsum_create("multi(1," + key1.pubkey + "," + key2.pubkey + ")"),
|
||||
"timestamp": "now"},
|
||||
success=True)
|
||||
self.log.info("Should not treat individual keys from the imported bare multisig as watchonly")
|
||||
test_address(w1,
|
||||
key1.p2pkh_addr,
|
||||
ismine=False)
|
||||
|
||||
# # Test ranged descriptors
|
||||
xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
|
||||
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||
addresses = ["2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] # hdkeypath=m/0'/0'/0' and 1'
|
||||
addresses += ["bcrt1qrd3n235cj2czsfmsuvqqpr3lu6lg0ju7scl8gn", "bcrt1qfqeppuvj0ww98r6qghmdkj70tv8qpchehegrg8"] # wpkh subscripts corresponding to the above addresses
|
||||
desc = "sh(wpkh(" + xpub + "/0/0/*" + "))"
|
||||
|
||||
self.log.info("Ranged descriptors cannot have labels")
|
||||
self.test_importdesc({"desc":descsum_create(desc),
|
||||
"timestamp": "now",
|
||||
"range": [0, 100],
|
||||
"label": "test"},
|
||||
success=False,
|
||||
error_code=-8,
|
||||
error_message='Ranged descriptors should not have a label')
|
||||
|
||||
self.log.info("Private keys required for private keys enabled wallet")
|
||||
self.test_importdesc({"desc":descsum_create(desc),
|
||||
"timestamp": "now",
|
||||
"range": [0, 100]},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message='Cannot import descriptor without private keys to a wallet with private keys enabled',
|
||||
wallet=wpriv)
|
||||
|
||||
self.log.info("Ranged descriptor import should warn without a specified range")
|
||||
self.test_importdesc({"desc": descsum_create(desc),
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
warnings=['Range not given, using default keypool range'])
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
# # Test importing of a ranged descriptor with xpriv
|
||||
self.log.info("Should not import a ranged descriptor that includes xpriv into a watch-only wallet")
|
||||
desc = "sh(wpkh(" + xpriv + "/0'/0'/*'" + "))"
|
||||
self.test_importdesc({"desc": descsum_create(desc),
|
||||
"timestamp": "now",
|
||||
"range": 1},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message='Cannot import private keys to a wallet with private keys disabled')
|
||||
for address in addresses:
|
||||
test_address(w1,
|
||||
address,
|
||||
ismine=False,
|
||||
solvable=False)
|
||||
|
||||
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": -1},
|
||||
success=False, error_code=-8, error_message='End of range is too high')
|
||||
|
||||
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [-1, 10]},
|
||||
success=False, error_code=-8, error_message='Range should be greater or equal than 0')
|
||||
|
||||
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]},
|
||||
success=False, error_code=-8, error_message='End of range is too high')
|
||||
|
||||
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [2, 1]},
|
||||
success=False, error_code=-8, error_message='Range specified as [begin,end] must not have begin after end')
|
||||
|
||||
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]},
|
||||
success=False, error_code=-8, error_message='Range is too large')
|
||||
|
||||
# Make sure ranged imports import keys in order
|
||||
w1 = self.nodes[1].get_wallet_rpc('w1')
|
||||
self.log.info('Key ranges should be imported in order')
|
||||
xpub = "tpubDAXcJ7s7ZwicqjprRaEWdPoHKrCS215qxGYxpusRLLmJuT69ZSicuGdSfyvyKpvUNYBW1s2U3NSrT6vrCYB9e6nZUEvrqnwXPF8ArTCRXMY"
|
||||
addresses = [
|
||||
'bcrt1qtmp74ayg7p24uslctssvjm06q5phz4yrxucgnv', # m/0'/0'/0
|
||||
'bcrt1q8vprchan07gzagd5e6v9wd7azyucksq2xc76k8', # m/0'/0'/1
|
||||
'bcrt1qtuqdtha7zmqgcrr26n2rqxztv5y8rafjp9lulu', # m/0'/0'/2
|
||||
'bcrt1qau64272ymawq26t90md6an0ps99qkrse58m640', # m/0'/0'/3
|
||||
'bcrt1qsg97266hrh6cpmutqen8s4s962aryy77jp0fg0', # m/0'/0'/4
|
||||
]
|
||||
|
||||
self.test_importdesc({'desc': descsum_create('wpkh([80002067/0h/0h]' + xpub + '/*)'),
|
||||
'active': True,
|
||||
'range' : [0, 2],
|
||||
'timestamp': 'now'
|
||||
},
|
||||
success=True)
|
||||
self.test_importdesc({'desc': descsum_create('sh(wpkh([abcdef12/0h/0h]' + xpub + '/*))'),
|
||||
'active': True,
|
||||
'range' : [0, 2],
|
||||
'timestamp': 'now'
|
||||
},
|
||||
success=True)
|
||||
self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'),
|
||||
'active': True,
|
||||
'range' : [0, 2],
|
||||
'timestamp': 'now'
|
||||
},
|
||||
success=True)
|
||||
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 5 * 3)
|
||||
for i, expected_addr in enumerate(addresses):
|
||||
received_addr = w1.getnewaddress('', 'bech32')
|
||||
assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'bech32')
|
||||
assert_equal(received_addr, expected_addr)
|
||||
bech32_addr_info = w1.getaddressinfo(received_addr)
|
||||
assert_equal(bech32_addr_info['desc'][:23], 'wpkh([80002067/0\'/0\'/{}]'.format(i))
|
||||
|
||||
shwpkh_addr = w1.getnewaddress('', 'p2sh-segwit')
|
||||
shwpkh_addr_info = w1.getaddressinfo(shwpkh_addr)
|
||||
assert_equal(shwpkh_addr_info['desc'][:26], 'sh(wpkh([abcdef12/0\'/0\'/{}]'.format(i))
|
||||
|
||||
pkh_addr = w1.getnewaddress('', 'legacy')
|
||||
pkh_addr_info = w1.getaddressinfo(pkh_addr)
|
||||
assert_equal(pkh_addr_info['desc'][:22], 'pkh([12345678/0\'/0\'/{}]'.format(i))
|
||||
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 4 * 3) # After retrieving a key, we don't refill the keypool again, so it's one less for each address type
|
||||
w1.keypoolrefill()
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 5 * 3)
|
||||
|
||||
# Check active=False default
|
||||
self.log.info('Check imported descriptors are not active by default')
|
||||
self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'),
|
||||
'range' : [0, 2],
|
||||
'timestamp': 'now',
|
||||
'internal': True
|
||||
},
|
||||
success=True)
|
||||
assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'legacy')
|
||||
|
||||
# # Test importing a descriptor containing a WIF private key
|
||||
wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh"
|
||||
address = "2MuhcG52uHPknxDgmGPsV18jSHFBnnRgjPg"
|
||||
desc = "sh(wpkh(" + wif_priv + "))"
|
||||
self.log.info("Should import a descriptor with a WIF private key as spendable")
|
||||
self.test_importdesc({"desc": descsum_create(desc),
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wpriv)
|
||||
test_address(wpriv,
|
||||
address,
|
||||
solvable=True,
|
||||
ismine=True)
|
||||
txid = w0.sendtoaddress(address, 49.99995540)
|
||||
w0.generatetoaddress(6, w0.getnewaddress())
|
||||
self.sync_blocks()
|
||||
tx = wpriv.createrawtransaction([{"txid": txid, "vout": 0}], {w0.getnewaddress(): 49.999})
|
||||
signed_tx = wpriv.signrawtransactionwithwallet(tx)
|
||||
w1.sendrawtransaction(signed_tx['hex'])
|
||||
|
||||
# Make sure that we can use import and use multisig as addresses
|
||||
self.log.info('Test that multisigs can be imported, signed for, and getnewaddress\'d')
|
||||
self.nodes[1].createwallet(wallet_name="wmulti_priv", disable_private_keys=False, blank=True, descriptors=True)
|
||||
wmulti_priv = self.nodes[1].get_wallet_rpc("wmulti_priv")
|
||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/0h/0h/*))#m2sr93jn",
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_priv)
|
||||
self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/1h/0h/*))#q3sztvx5",
|
||||
"active": True,
|
||||
"internal" : True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_priv)
|
||||
|
||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated
|
||||
addr = wmulti_priv.getnewaddress('', 'bech32')
|
||||
assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
|
||||
change_addr = wmulti_priv.getrawchangeaddress('bech32')
|
||||
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
|
||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
|
||||
txid = w0.sendtoaddress(addr, 10)
|
||||
self.nodes[0].generate(6)
|
||||
send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8)
|
||||
decoded = wmulti_priv.decoderawtransaction(wmulti_priv.gettransaction(send_txid)['hex'])
|
||||
assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
|
||||
self.nodes[0].generate(6)
|
||||
self.sync_all()
|
||||
|
||||
self.nodes[1].createwallet(wallet_name="wmulti_pub", disable_private_keys=True, blank=True, descriptors=True)
|
||||
wmulti_pub = self.nodes[1].get_wallet_rpc("wmulti_pub")
|
||||
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))#tsry0s5e",
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_pub)
|
||||
self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))#c08a2rzv",
|
||||
"active": True,
|
||||
"internal" : True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_pub)
|
||||
|
||||
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used
|
||||
addr = wmulti_pub.getnewaddress('', 'bech32')
|
||||
assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
|
||||
change_addr = wmulti_pub.getrawchangeaddress('bech32')
|
||||
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
|
||||
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
|
||||
txid = w0.sendtoaddress(addr, 10)
|
||||
vout = find_vout_for_address(self.nodes[0], txid, addr)
|
||||
self.nodes[0].generate(6)
|
||||
self.sync_all()
|
||||
assert_equal(wmulti_pub.getbalance(), wmulti_priv.getbalance())
|
||||
|
||||
self.log.info("Multisig with distributed keys")
|
||||
self.nodes[1].createwallet(wallet_name="wmulti_priv1", descriptors=True)
|
||||
wmulti_priv1 = self.nodes[1].get_wallet_rpc("wmulti_priv1")
|
||||
res = wmulti_priv1.importdescriptors([
|
||||
{
|
||||
"desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"),
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
},
|
||||
{
|
||||
"desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"),
|
||||
"active": True,
|
||||
"internal" : True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
}])
|
||||
assert_equal(res[0]['success'], True)
|
||||
assert_equal(res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||
assert_equal(res[1]['success'], True)
|
||||
assert_equal(res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||
|
||||
self.nodes[1].createwallet(wallet_name='wmulti_priv2', blank=True, descriptors=True)
|
||||
wmulti_priv2 = self.nodes[1].get_wallet_rpc('wmulti_priv2')
|
||||
res = wmulti_priv2.importdescriptors([
|
||||
{
|
||||
"desc": descsum_create("wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"),
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
},
|
||||
{
|
||||
"desc": descsum_create("wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"),
|
||||
"active": True,
|
||||
"internal" : True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"
|
||||
}])
|
||||
assert_equal(res[0]['success'], True)
|
||||
assert_equal(res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||
assert_equal(res[1]['success'], True)
|
||||
assert_equal(res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||
|
||||
rawtx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {w0.getnewaddress(): 9.999})
|
||||
tx_signed_1 = wmulti_priv1.signrawtransactionwithwallet(rawtx)
|
||||
assert_equal(tx_signed_1['complete'], False)
|
||||
tx_signed_2 = wmulti_priv2.signrawtransactionwithwallet(tx_signed_1['hex'])
|
||||
assert_equal(tx_signed_2['complete'], True)
|
||||
self.nodes[1].sendrawtransaction(tx_signed_2['hex'])
|
||||
|
||||
self.log.info("Combo descriptors cannot be active")
|
||||
self.test_importdesc({"desc": descsum_create("combo(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"),
|
||||
"active": True,
|
||||
"range": 1,
|
||||
"timestamp": "now"},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message="Combo descriptors cannot be set to active")
|
||||
|
||||
self.log.info("Descriptors with no type cannot be active")
|
||||
self.test_importdesc({"desc": descsum_create("pk(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"),
|
||||
"active": True,
|
||||
"range": 1,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
warnings=["Unknown output type, cannot set descriptor to active."])
|
||||
|
||||
if __name__ == '__main__':
|
||||
ImportDescriptorsTest().main()
|
|
@ -22,16 +22,63 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
addr_before_encrypting = nodes[0].getnewaddress()
|
||||
addr_before_encrypting_data = nodes[0].getaddressinfo(addr_before_encrypting)
|
||||
wallet_info_old = nodes[0].getwalletinfo()
|
||||
assert addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid']
|
||||
if not self.options.descriptors:
|
||||
assert addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid']
|
||||
|
||||
# Encrypt wallet and wait to terminate
|
||||
nodes[0].encryptwallet('test')
|
||||
if self.options.descriptors:
|
||||
# Import hardened derivation only descriptors
|
||||
nodes[0].walletpassphrase('test', 10)
|
||||
nodes[0].importdescriptors([
|
||||
{
|
||||
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1h/*h)#a0nyvl0k",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"desc": "sh(wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/2h/*h))#lmeu2axg",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/3h/*h)#jkl636gm",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True,
|
||||
"internal": True
|
||||
},
|
||||
{
|
||||
"desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/4h/*h)#l3crwaus",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True,
|
||||
"internal": True
|
||||
},
|
||||
{
|
||||
"desc": "sh(wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/5h/*h))#qg8wa75f",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True,
|
||||
"internal": True
|
||||
}
|
||||
])
|
||||
nodes[0].walletlock()
|
||||
# Keep creating keys
|
||||
addr = nodes[0].getnewaddress()
|
||||
addr_data = nodes[0].getaddressinfo(addr)
|
||||
wallet_info = nodes[0].getwalletinfo()
|
||||
assert addr_before_encrypting_data['hdseedid'] != wallet_info['hdseedid']
|
||||
assert addr_data['hdseedid'] == wallet_info['hdseedid']
|
||||
assert addr_before_encrypting_data['hdmasterfingerprint'] != addr_data['hdmasterfingerprint']
|
||||
if not self.options.descriptors:
|
||||
assert addr_data['hdseedid'] == wallet_info['hdseedid']
|
||||
assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress)
|
||||
|
||||
# put six (plus 2) new keys in the keypool (100% external-, +100% internal-keys, 1 in min)
|
||||
|
@ -39,8 +86,12 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
nodes[0].keypoolrefill(6)
|
||||
nodes[0].walletlock()
|
||||
wi = nodes[0].getwalletinfo()
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 6)
|
||||
assert_equal(wi['keypoolsize'], 6)
|
||||
if self.options.descriptors:
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 18)
|
||||
assert_equal(wi['keypoolsize'], 18)
|
||||
else:
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 6)
|
||||
assert_equal(wi['keypoolsize'], 6)
|
||||
|
||||
# drain the internal keys
|
||||
nodes[0].getrawchangeaddress()
|
||||
|
@ -80,11 +131,15 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
nodes[0].walletpassphrase('test', 100)
|
||||
nodes[0].keypoolrefill(100)
|
||||
wi = nodes[0].getwalletinfo()
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 100)
|
||||
assert_equal(wi['keypoolsize'], 100)
|
||||
if self.options.descriptors:
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 300)
|
||||
assert_equal(wi['keypoolsize'], 300)
|
||||
else:
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 100)
|
||||
assert_equal(wi['keypoolsize'], 100)
|
||||
|
||||
# create a blank wallet
|
||||
nodes[0].createwallet(wallet_name='w2', blank=True)
|
||||
nodes[0].createwallet(wallet_name='w2', blank=True, disable_private_keys=True)
|
||||
w2 = nodes[0].get_wallet_rpc('w2')
|
||||
|
||||
# refer to initial wallet as w1
|
||||
|
@ -92,8 +147,11 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
|
||||
# import private key and fund it
|
||||
address = addr.pop()
|
||||
privkey = w1.dumpprivkey(address)
|
||||
res = w2.importmulti([{'scriptPubKey': {'address': address}, 'keys': [privkey], 'timestamp': 'now'}])
|
||||
desc = w1.getaddressinfo(address)['desc']
|
||||
if self.options.descriptors:
|
||||
res = w2.importdescriptors([{'desc': desc, 'timestamp': 'now'}])
|
||||
else:
|
||||
res = w2.importmulti([{'desc': desc, 'timestamp': 'now'}])
|
||||
assert_equal(res[0]['success'], True)
|
||||
w1.walletpassphrase('test', 100)
|
||||
|
||||
|
|
|
@ -79,7 +79,15 @@ class KeypoolRestoreTest(BitcoinTestFramework):
|
|||
assert_equal(self.nodes[idx].getbalance(), 15)
|
||||
assert_equal(self.nodes[idx].listtransactions()[0]['category'], "receive")
|
||||
# Check that we have marked all keys up to the used keypool key as used
|
||||
assert_equal(self.nodes[idx].getaddressinfo(self.nodes[idx].getnewaddress())['hdkeypath'], "m/0'/0'/110'")
|
||||
if self.options.descriptors:
|
||||
if output_type == 'legacy':
|
||||
assert_equal(self.nodes[idx].getaddressinfo(self.nodes[idx].getnewaddress(address_type=output_type))['hdkeypath'], "m/44'/1'/0'/0/110")
|
||||
elif output_type == 'p2sh-segwit':
|
||||
assert_equal(self.nodes[idx].getaddressinfo(self.nodes[idx].getnewaddress(address_type=output_type))['hdkeypath'], "m/49'/1'/0'/0/110")
|
||||
elif output_type == 'bech32':
|
||||
assert_equal(self.nodes[idx].getaddressinfo(self.nodes[idx].getnewaddress(address_type=output_type))['hdkeypath'], "m/84'/1'/0'/0/110")
|
||||
else:
|
||||
assert_equal(self.nodes[idx].getaddressinfo(self.nodes[idx].getnewaddress(address_type=output_type))['hdkeypath'], "m/0'/0'/110'")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -115,15 +115,16 @@ class WalletLabelsTest(BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-11, "No addresses with label", node.getaddressesbylabel, "")
|
||||
|
||||
# Check that addmultisigaddress can assign labels.
|
||||
for label in labels:
|
||||
addresses = []
|
||||
for x in range(10):
|
||||
addresses.append(node.getnewaddress())
|
||||
multisig_address = node.addmultisigaddress(5, addresses, label.name)['address']
|
||||
label.add_address(multisig_address)
|
||||
label.purpose[multisig_address] = "send"
|
||||
label.verify(node)
|
||||
node.generate(101)
|
||||
if not self.options.descriptors:
|
||||
for label in labels:
|
||||
addresses = []
|
||||
for x in range(10):
|
||||
addresses.append(node.getnewaddress())
|
||||
multisig_address = node.addmultisigaddress(5, addresses, label.name)['address']
|
||||
label.add_address(multisig_address)
|
||||
label.purpose[multisig_address] = "send"
|
||||
label.verify(node)
|
||||
node.generate(101)
|
||||
|
||||
# Check that setlabel can change the label of an address from a
|
||||
# different label.
|
||||
|
|
Loading…
Add table
Reference in a new issue