mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 03:52:49 +01:00
Merge #16528: Native Descriptor Wallets using DescriptorScriptPubKeyMan
223588b1bbc63dc57098bbd0baa48635e0cc0b82 Add a --descriptors option to various tests (Andrew Chow) 869f7ab30aeb4d7fbd563c535b55467a8a0430cf tests: Add RPCOverloadWrapper which overloads some disabled RPCs (Andrew Chow) cf060628590fab87d73f278e744d70ef2d5d81db Correctly check for default wallet (Andrew Chow) 886e0d75f5fea2421190aa4812777d89f68962cc Implement CWallet::IsSpentKey for non-LegacySPKMans (Andrew Chow) 3c19fdd2a2fd5394fcfa75b2ba84ab2277cbdabf Return error when no ScriptPubKeyMan is available for specified type (Andrew Chow) 388ba94231f2f10a0be751c562cdd4650510a90a Change wallet_encryption.py to use signmessage instead of dumpprivkey (Andrew Chow) 1346e14831489f9c8f53a08f9dfed61d55d53c6f Functional tests for descriptor wallets (Andrew Chow) f193ea889ddb53d9a5c47647966681d525e38368 add importdescriptors RPC and tests for native descriptor wallets (Hugo Nguyen) ce24a944940019185efebcc5d85eac458ed26016 Add IsLegacy to CWallet so that the GUI knows whether to show watchonly (Andrew Chow) 1cb42b22b11c27e64462afc25a94b2fc50bfa113 Generate new descriptors when encrypting (Andrew Chow) 82ae02b1656819f4bd5023b8955447e1d4ea8692 Be able to create new wallets with DescriptorScriptPubKeyMans as backing (Andrew Chow) b713baa75a62335ab9c0eed9ef76a95bfec30668 Implement GetMetadata in DescriptorScriptPubKeyMan (Andrew Chow) 8b9603bd0b443e2f7984eb72bf2e21cf02af0bcb Change GetMetadata to use unique_ptr<CKeyMetadata> (Andrew Chow) 72a9540df96ffdb94f039b9c14eaacdc7d961196 Implement FillPSBT in DescriptorScriptPubKeyMan (Andrew Chow) 84b4978c02102171775c77a45f6ec198930f0a88 Implement SignMessage for descriptor wallets (Andrew Chow) bde7c9fa38775a81d53ac0484fa9c98076a0c7d1 Implement SignTransaction in DescriptorScriptPubKeyMan (Andrew Chow) d50c8ddd4190f20bf0debd410348b73408ec3143 Implement GetSolvingProvider for DescriptorScriptPubKeyMan (Andrew Chow) f1ca5feb4ad668a3e1ae543d0addd5f483f1a88f Implement GetKeypoolOldestTime and only display it if greater than 0 (Andrew Chow) 586b57a9a6b4b12a78f792785b63a5a1743bce0c Implement ReturnDestination in DescriptorScriptPubKeyMan (Andrew Chow) f866957979c23cefd41efa9dae9e53b9177818dc Implement GetReservedDestination in DescriptorScriptPubKeyMan (Andrew Chow) a775f7c7fd0b9094fcbeee6ba92206d5bbb19164 Implement Unlock and Encrypt in DescriptorScriptPubKeyMan (Andrew Chow) bfdd0734869a22217c15858d7a76d0dacc2ebc86 Implement GetNewDestination for DescriptorScriptPubKeyMan (Andrew Chow) 58c7651821b0eeff0a99dc61d78d2e9e07986580 Implement TopUp in DescriptorScriptPubKeyMan (Andrew Chow) e014886a342508f7c8d80323eee9a5f314eaf94c Implement SetupGeneration for DescriptorScriptPubKeyMan (Andrew Chow) 46dfb99768e7d03a3cf552812d5b41ceaebc06be Implement writing descriptorkeys, descriptorckeys, and descriptors to wallet file (Andrew Chow) 4cb9b69be031e1dc65d8964794781b347fd948f5 Implement several simple functions in DescriptorScriptPubKeyMan (Andrew Chow) d1ec3e4f19487b4b100f80ad02eac063c571777d Add IsSingleType to Descriptors (Andrew Chow) 953feb3d2724f5398dd48990c4957a19313d2c8c Implement loading of keys for DescriptorScriptPubKeyMan (Andrew Chow) 2363e9fcaa41b68bf11153f591b95f2d41ff9a1a Load the descriptor cache from the wallet file (Andrew Chow) 46c46aebb7943e1e2e96755e94dc6c197920bf75 Implement GetID for DescriptorScriptPubKeyMan (Andrew Chow) ec2f9e1178c8e38c0a5ca063fe81adac8f916348 Implement IsHDEnabled in DescriptorScriptPubKeyMan (Andrew Chow) 741122d4c1a62ced3e96d16d67f4eeb3a6522d99 Implement MarkUnusedAddresses in DescriptorScriptPubKeyMan (Andrew Chow) 2db7ca765c8fb2c71dd6f7c4f29ad70e68ff1720 Implement IsMine for DescriptorScriptPubKeyMan (Andrew Chow) db7177af8c159abbcc209f2caafcd45d54c181c5 Add LoadDescriptorScriptPubKeyMan and SetActiveScriptPubKeyMan to CWallet (Andrew Chow) 78f8a92910d34247fa5d04368338c598d9908267 Implement SetType in DescriptorScriptPubKeyMan (Andrew Chow) 834de0300cde57ca3f662fb7aa5b1bdaed68bc8f Store WalletDescriptor in DescriptorScriptPubKeyMan (Andrew Chow) d8132669e10c1db9ae0c2ea0d3f822d7d2f01345 Add a lock cs_desc_man for DescriptorScriptPubKeyMan (Andrew Chow) 3194a7f88ac1a32997b390b4f188c4b6a4af04a5 Introduce WalletDescriptor class (Andrew Chow) 6b13cd3fa854dfaeb9e269bff3d67cacc0e5b5dc Create LegacyScriptPubKeyMan when not a descriptor wallet (Andrew Chow) aeac157c9dc141546b45e06ba9c2e641ad86083f Return nullptr from GetLegacyScriptPubKeyMan if descriptor wallet (Andrew Chow) 96accc73f067c7c95946e9932645dd821ef67f63 Add WALLET_FLAG_DESCRIPTORS (Andrew Chow) 6b8119af53ee2fdb4c4b5b24b4e650c0dc3bd27c Introduce DescriptorScriptPubKeyMan as a dummy class (Andrew Chow) 06620302c713cae65ee8e4ff9302e4c88e2a1285 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: utACK 223588b1bbc63dc57098bbd0baa48635e0cc0b82 (rebased, nits addressed) jonatack: Code review re-ACK 223588b1bbc63dc57098bbd0baa48635e0cc0b82. fjahr: re-ACK 223588b1bbc63dc57098bbd0baa48635e0cc0b82 instagibbs: light re-ACK 223588b meshcollider: Code review ACK 223588b1bbc63dc57098bbd0baa48635e0cc0b82 Tree-SHA512: 59bc52aeddbb769ed5f420d5d240d8137847ac821b588eb616b34461253510c1717d6a70bab8765631738747336ae06f45ba39603ccd17f483843e5ed9a90986 Introduce SetType function to tell ScriptPubKeyMans the type and internal-ness of it Introduce DescriptorScriptPubKeyMan as a dummy class Add WALLET_FLAG_DESCRIPTORS Return nullptr from GetLegacyScriptPubKeyMan if descriptor wallet Create LegacyScriptPubKeyMan when not a descriptor wallet Introduce WalletDescriptor class WalletDescriptor is a Descriptor with other wallet metadata Add a lock cs_desc_man for DescriptorScriptPubKeyMan Store WalletDescriptor in DescriptorScriptPubKeyMan Implement SetType in DescriptorScriptPubKeyMan Add LoadDescriptorScriptPubKeyMan and SetActiveScriptPubKeyMan to CWallet Implement IsMine for DescriptorScriptPubKeyMan Adds a set of scriptPubKeys that DescriptorScriptPubKeyMan tracks. If the given script is in that set, it is considered ISMINE_SPENDABLE Implement MarkUnusedAddresses in DescriptorScriptPubKeyMan Implement IsHDEnabled in DescriptorScriptPubKeyMan Implement GetID for DescriptorScriptPubKeyMan Load the descriptor cache from the wallet file Implement loading of keys for DescriptorScriptPubKeyMan Add IsSingleType to Descriptors IsSingleType will return whether the descriptor will give one or multiple scriptPubKeys Implement several simple functions in DescriptorScriptPubKeyMan Implements a bunch of one liners: UpgradeKeyMetadata, IsFirstRun, HavePrivateKeys, KeypoolCountExternalKeys, GetKeypoolSize, GetTimeFirstKey, CanGetAddresses, RewriteDB Implement writing descriptorkeys, descriptorckeys, and descriptors to wallet file Implement SetupGeneration for DescriptorScriptPubKeyMan Implement TopUp in DescriptorScriptPubKeyMan Implement GetNewDestination for DescriptorScriptPubKeyMan Implement Unlock and Encrypt in DescriptorScriptPubKeyMan Implement GetReservedDestination in DescriptorScriptPubKeyMan Implement ReturnDestination in DescriptorScriptPubKeyMan Implement GetKeypoolOldestTime and only display it if greater than 0 Implement GetSolvingProvider for DescriptorScriptPubKeyMan Internally, a GetSigningProvider function is introduced which allows for some private keys to be optionally included. This can be called with a script as the argument (i.e. a scriptPubKey from our wallet when we are signing) or with a pubkey. In order to know what index to expand the private keys for that pubkey, we need to also cache all of the pubkeys involved when we expand the descriptor. So SetCache and TopUp are updated to do this too. Implement SignTransaction in DescriptorScriptPubKeyMan Implement SignMessage for descriptor wallets Implement FillPSBT in DescriptorScriptPubKeyMan FillPSBT will add our own scripts to the PSBT if those inputs are ours. If an input also lists pubkeys that we happen to know the private keys for, we will sign those inputs too. Change GetMetadata to use unique_ptr<CKeyMetadata> Implement GetMetadata in DescriptorScriptPubKeyMan Be able to create new wallets with DescriptorScriptPubKeyMans as backing Generate new descriptors when encrypting Add IsLegacy to CWallet so that the GUI knows whether to show watchonly add importdescriptors RPC and tests for native descriptor wallets Co-authored-by: Andrew Chow <achow101-github@achow101.com> Functional tests for descriptor wallets Change wallet_encryption.py to use signmessage instead of dumpprivkey Return error when no ScriptPubKeyMan is available for specified type When a CWallet doesn't have a ScriptPubKeyMan for the requested type in GetNewDestination, give a meaningful error. Also handle this in Qt which did not do anything with errors. Implement CWallet::IsSpentKey for non-LegacySPKMans tests: Add RPCOverloadWrapper which overloads some disabled RPCs RPCOverloadWrapper overloads some deprecated or disabled RPCs with an implementation using other RPCs to avoid having a ton of code churn around replacing those RPCs. Add a --descriptors option to various tests Adds a --descriptors option globally to the test framework. This will make the test create and use descriptor wallets. However some tests may not work with this. Some tests are modified to work with --descriptors and run with that option in test_runer: * wallet_basic.py * wallet_encryption.py * wallet_keypool.py <---- wallet_keypool_hd.py actually * wallet_keypool_topup.py * wallet_labels.py * wallet_avoidreuse.py
This commit is contained in:
parent
4064334732
commit
f293c046f4
@ -287,6 +287,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;
|
||||
|
@ -256,6 +256,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;
|
||||
|
@ -77,3 +77,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;
|
||||
|
@ -99,6 +99,24 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -208,21 +208,29 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances)
|
||||
{
|
||||
int unit = walletModel->getOptionsModel()->getDisplayUnit();
|
||||
m_balances = balances;
|
||||
if (walletModel->wallet().privateKeysDisabled()) {
|
||||
ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
if (walletModel->wallet().isLegacy()) {
|
||||
if (walletModel->wallet().privateKeysDisabled()) {
|
||||
ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
} else {
|
||||
ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelAnonymized->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.anonymized_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchAvailable->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchPending->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
}
|
||||
} else {
|
||||
ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelAnonymized->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.anonymized_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchAvailable->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchPending->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelWatchTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelUnconfirmed->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.unconfirmed_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelImmature->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelAnonymized->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.anonymized_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
ui->labelTotal->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
|
||||
}
|
||||
// only show immature (newly mined) balance if it's non-zero, so as not to complicate things
|
||||
// for the non-mining users
|
||||
|
@ -137,17 +137,40 @@ void ReceiveCoinsDialog::on_receiveButton_clicked()
|
||||
QString label = ui->reqLabel->text();
|
||||
/* Generate new receiving address */
|
||||
address = model->getAddressTableModel()->addRow(AddressTableModel::Receive, label, "");
|
||||
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 address"),
|
||||
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)
|
||||
|
@ -261,6 +261,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] {
|
||||
std::unique_ptr<interfaces::Wallet> wallet = node().walletLoader().createWallet(name, m_passphrase, flags, m_error_message, m_warning_message);
|
||||
|
@ -153,6 +153,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" },
|
||||
@ -208,7 +209,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "createwallet", 1, "disable_private_keys"},
|
||||
{ "createwallet", 2, "blank"},
|
||||
{ "createwallet", 4, "avoid_reuse"},
|
||||
{ "createwallet", 5, "load_on_startup"},
|
||||
{ "createwallet", 5, "descriptors"},
|
||||
{ "createwallet", 6, "load_on_startup"},
|
||||
{ "loadwallet", 1, "load_on_startup"},
|
||||
{ "unloadwallet", 1, "load_on_startup"},
|
||||
{ "upgradetohd", 3, "rescan"},
|
||||
|
@ -572,6 +572,7 @@ public:
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed raw(H) descriptor. */
|
||||
@ -596,6 +597,7 @@ public:
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed pk(P) descriptor. */
|
||||
@ -606,6 +608,7 @@ protected:
|
||||
public:
|
||||
PKDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pk") {}
|
||||
std::optional<OutputType> GetOutputType() const override { return OutputType::LEGACY; }
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed pkh(P) descriptor. */
|
||||
@ -621,6 +624,7 @@ protected:
|
||||
public:
|
||||
PKHDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pkh") {}
|
||||
std::optional<OutputType> GetOutputType() const override { return OutputType::LEGACY; }
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed multi(...) or sortedmulti(...) descriptor */
|
||||
@ -640,6 +644,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. */
|
||||
@ -655,6 +660,7 @@ public:
|
||||
assert(m_subdescriptor_arg);
|
||||
return OutputType::LEGACY;
|
||||
}
|
||||
bool IsSingleType() const final { return true; }
|
||||
};
|
||||
|
||||
/** A parsed combo(P) descriptor. */
|
||||
@ -679,6 +685,7 @@ protected:
|
||||
public:
|
||||
ComboDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), {}, "combo") {}
|
||||
std::optional<OutputType> GetOutputType() const override { return OutputType::LEGACY; }
|
||||
bool IsSingleType() const final { return false; }
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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;
|
||||
|
||||
|
@ -516,6 +516,7 @@ public:
|
||||
{
|
||||
RemoveWallet(m_wallet, false /* load_on_start */);
|
||||
}
|
||||
bool isLegacy() override { return m_wallet->IsLegacy(); }
|
||||
std::unique_ptr<Handler> handleUnload(UnloadFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyUnload.connect(fn));
|
||||
|
@ -1615,3 +1615,294 @@ 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(), 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) {
|
||||
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 wallet>\" }]'")
|
||||
},
|
||||
}.Check(main_request);
|
||||
|
||||
// Acquire the wallet
|
||||
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(main_request);
|
||||
if (!wallet) return NullUniValue;
|
||||
|
||||
CWallet* const pwallet = wallet.get();
|
||||
|
||||
// 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);
|
||||
{
|
||||
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 */);
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -1867,7 +1867,7 @@ static UniValue keypoolrefill(const JSONRPCRequest& request)
|
||||
if (!wallet) return NullUniValue;
|
||||
CWallet* const pwallet = wallet.get();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@ -2498,7 +2498,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
{RPCResult::Type::NUM, "immature_balance", "DEPRECATED. Identical to getbalances().mine.immature"},
|
||||
{RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"},
|
||||
{RPCResult::Type::NUM_TIME, "timefirstkey", "the " + UNIX_EPOCH_TIME + " of the oldest known key 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, "keys_left", "how many new keys are left since last automatic backup"},
|
||||
@ -2515,13 +2515,14 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
{RPCResult::Type::NUM, "hdinternalkeyindex", "current internal childkey index"},
|
||||
}},
|
||||
}},
|
||||
{RPCResult::Type::BOOL, "private_keys_enabled", "false if privatekeys are disabled for this wallet (enforced watch-only wallet)"},
|
||||
{RPCResult::Type::BOOL, "avoid_reuse", "whether this wallet tracks clean/dirty coins in terms of reuse"},
|
||||
{RPCResult::Type::OBJ, "scanning", "current scanning details, or false if no scan is in progress",
|
||||
{
|
||||
{RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"},
|
||||
{RPCResult::Type::NUM, "progress", "scanning progress percentage [0.0, 1.0]"},
|
||||
}},
|
||||
{RPCResult::Type::BOOL, "private_keys_enabled", "false if privatekeys are disabled for this wallet (enforced watch-only wallet)"},
|
||||
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
|
||||
},
|
||||
},
|
||||
RPCExamples{
|
||||
@ -2546,6 +2547,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
UniValue obj(UniValue::VOBJ);
|
||||
|
||||
const auto bal = pwallet->GetBalance();
|
||||
int64_t kp_oldest = pwallet->GetOldestKeyPoolTime();
|
||||
obj.pushKV("walletname", pwallet->GetName());
|
||||
obj.pushKV("walletversion", pwallet->GetVersion());
|
||||
obj.pushKV("format", pwallet->GetDatabase().Format());
|
||||
@ -2554,9 +2556,12 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
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());
|
||||
// TODO: implement timefirstkey for Descriptor KeyMan or explain why it's not provided
|
||||
if (spk_man) {
|
||||
obj.pushKV("timefirstkey", spk_man->GetTimeFirstKey());
|
||||
obj.pushKV("keypoololdest", spk_man->GetOldestKeyPoolTime());
|
||||
}
|
||||
if (kp_oldest > 0) {
|
||||
obj.pushKV("keypoololdest", kp_oldest);
|
||||
}
|
||||
size_t kpExternalSize = pwallet->KeypoolCountExternalKeys();
|
||||
obj.pushKV("keypoolsize", (int64_t)kpExternalSize);
|
||||
@ -2584,6 +2589,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
}
|
||||
obj.pushKV("hdaccounts", accounts);
|
||||
}
|
||||
obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
|
||||
obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE));
|
||||
if (pwallet->IsScanning()) {
|
||||
UniValue scanning(UniValue::VOBJ);
|
||||
@ -2593,7 +2599,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
||||
} else {
|
||||
obj.pushKV("scanning", false);
|
||||
}
|
||||
obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
|
||||
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
||||
return obj;
|
||||
}
|
||||
|
||||
@ -2888,6 +2894,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 upgradetohd."},
|
||||
{"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"},
|
||||
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "null", "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
|
||||
},
|
||||
RPCResult{
|
||||
@ -2926,6 +2933,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;
|
||||
}
|
||||
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
@ -2933,7 +2943,7 @@ static UniValue createwallet(const JSONRPCRequest& request)
|
||||
options.create_flags = flags;
|
||||
options.create_passphrase = passphrase;
|
||||
bilingual_str error;
|
||||
std::optional<bool> load_on_start = request.params[5].isNull() ? std::nullopt : std::optional<bool>(request.params[5].get_bool());
|
||||
std::optional<bool> load_on_start = request.params[6].isNull() ? std::nullopt : std::optional<bool>(request.params[6].get_bool());
|
||||
std::shared_ptr<CWallet> wallet = CreateWallet(*context.chain, *context.m_coinjoin_loader, request.params[0].get_str(), load_on_start, options, status, error, warnings);
|
||||
if (!wallet) {
|
||||
RPCErrorCode code = status == DatabaseStatus::FAILED_ENCRYPT ? RPC_WALLET_ENCRYPTION_FAILED : RPC_WALLET_ERROR;
|
||||
@ -3814,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);
|
||||
CHDChain hdChainCurrent;
|
||||
LegacyScriptPubKeyMan* legacy_spk_man = pwallet->GetLegacyScriptPubKeyMan();
|
||||
@ -3972,6 +3982,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);
|
||||
UniValue dumphdinfo(const JSONRPCRequest& request);
|
||||
UniValue importelectrumwallet(const JSONRPCRequest& request);
|
||||
|
||||
@ -4200,7 +4211,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "abortrescan", &abortrescan, {} },
|
||||
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label"} },
|
||||
{ "wallet", "backupwallet", &backupwallet, {"destination"} },
|
||||
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "load_on_startup"} },
|
||||
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors", "load_on_startup"} },
|
||||
{ "wallet", "dumphdinfo", &dumphdinfo, {} },
|
||||
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
|
||||
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
|
||||
@ -4218,6 +4229,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
|
||||
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
|
||||
{ "wallet", "importelectrumwallet", &importelectrumwallet, {"filename", "index"} },
|
||||
{ "wallet", "importdescriptors", &importdescriptors, {"requests"} },
|
||||
{ "wallet", "importmulti", &importmulti, {"requests","options"} },
|
||||
{ "wallet", "importprivkey", &importprivkey, {"privkey","label","rescan"} },
|
||||
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
|
||||
|
@ -779,7 +779,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);
|
||||
|
||||
@ -787,14 +787,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 std::make_unique<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 std::make_unique<CKeyMetadata>(it->second);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
@ -1773,3 +1773,640 @@ bool LegacyScriptPubKeyMan::GetHDChain(CHDChain& hdChainRet) const
|
||||
hdChainRet = hdChain;
|
||||
return !hdChain.IsNull();
|
||||
}
|
||||
|
||||
void LegacyScriptPubKeyMan::SetType(bool internal) {}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::GetNewDestination(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
|
||||
|
||||
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;
|
||||
}
|
||||
const OutputType type{OutputType::LEGACY};
|
||||
std::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(bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
std::string error;
|
||||
bool result = GetNewDestination(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(WalletBatch &batch, const CScript& script, const std::optional<int64_t>& block_time)
|
||||
{
|
||||
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 = "pkh(" + xpub + "/44'";
|
||||
std::string desc_suffix = "/*)";
|
||||
|
||||
// Mainnet derives at 5', testnet and regtest derive at 1'
|
||||
if (Params().IsTestChain()) {
|
||||
desc_prefix += "/1'";
|
||||
} else {
|
||||
desc_prefix += "/5'";
|
||||
}
|
||||
|
||||
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 = std::make_unique<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 = std::make_unique<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;
|
||||
}
|
||||
|
||||
CKey key;
|
||||
if (!keys->GetKey(ToKeyID(pkhash), 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;
|
||||
}
|
||||
|
||||
// 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.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 = std::make_unique<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 = std::make_unique<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 = std::make_unique<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(bool internal)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -5,7 +5,9 @@
|
||||
#ifndef BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
|
||||
#define BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
|
||||
|
||||
#include <outputtype.h>
|
||||
#include <psbt.h>
|
||||
#include <script/descriptor.h>
|
||||
#include <script/signingprovider.h>
|
||||
#include <script/standard.h>
|
||||
#include <util/error.h>
|
||||
@ -196,7 +198,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; }
|
||||
|
||||
@ -214,6 +216,8 @@ public:
|
||||
|
||||
virtual uint256 GetID() const { return uint256(); }
|
||||
|
||||
virtual void SetType(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 {
|
||||
@ -348,7 +352,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;
|
||||
|
||||
@ -362,6 +366,8 @@ public:
|
||||
|
||||
uint256 GetID() const override;
|
||||
|
||||
void SetType(bool internal) override;
|
||||
|
||||
// Map from Key ID to key metadata.
|
||||
std::map<CKeyID, CKeyMetadata> mapKeyMetadata GUARDED_BY(cs_KeyStore);
|
||||
|
||||
@ -499,4 +505,110 @@ 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;
|
||||
|
||||
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, bool internal)
|
||||
: ScriptPubKeyMan(storage),
|
||||
m_internal(internal)
|
||||
{}
|
||||
|
||||
mutable RecursiveMutex cs_desc_man;
|
||||
|
||||
bool GetNewDestination(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(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(WalletBatch &batch, const CScript& script, const std::optional<int64_t>& block_time) 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(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
|
||||
|
@ -1365,6 +1365,27 @@ BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
|
||||
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2PKH_INPUT_SIZE + 1);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
SpanReader vr{0, 0, malformed_record, 0};
|
||||
WalletDescriptor w_desc;
|
||||
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
|
||||
{
|
||||
auto chain = interfaces::MakeChain(m_node);
|
||||
|
@ -310,7 +310,10 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, interfaces::Coin
|
||||
return nullptr;
|
||||
}
|
||||
if (!create_blank) {
|
||||
{
|
||||
if (wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
LOCK(wallet->cs_wallet);
|
||||
wallet->SetupDescriptorScriptPubKeyMans();
|
||||
} else {
|
||||
// TODO: drop this condition after removing option to create non-HD wallets
|
||||
// related backport bitcoin#11250
|
||||
if (wallet->GetVersion() >= FEATURE_HD) {
|
||||
@ -675,7 +678,10 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
|
||||
Lock();
|
||||
Unlock(strWalletPassphrase);
|
||||
|
||||
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 not using HD, generate new keypool
|
||||
if (spk_man->IsHDEnabled()) {
|
||||
if (!spk_man->TopUp()) {
|
||||
@ -811,13 +817,20 @@ 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)) {
|
||||
if (GetDestData(PKHash(keyid), "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)) {
|
||||
if (GetDestData(PKHash(keyid), "used", nullptr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1600,9 +1613,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;
|
||||
}
|
||||
@ -2928,11 +2942,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];
|
||||
@ -2944,33 +2964,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;
|
||||
}
|
||||
@ -3004,56 +3001,16 @@ 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);
|
||||
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
|
||||
int n_signed_this_spkm = 0;
|
||||
|
||||
if (PSBTInputSigned(input)) {
|
||||
continue;
|
||||
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
|
||||
if (res != TransactionError::OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Get the scriptPubKey to know which SigningProvider to use
|
||||
CScript script;
|
||||
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
|
||||
complete = false;
|
||||
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;
|
||||
}
|
||||
|
||||
int n_signed_this_spkm = 0;
|
||||
// Fill in the information from the spk_man
|
||||
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
|
||||
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());
|
||||
|
||||
if (n_signed) {
|
||||
(*n_signed) += n_signed_this_spkm;
|
||||
}
|
||||
if (n_signed) {
|
||||
(*n_signed) += n_signed_this_spkm;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4037,6 +3994,8 @@ bool CWallet::GetNewDestination(const std::string label, CTxDestination& dest, s
|
||||
if (spk_man) {
|
||||
spk_man->TopUp();
|
||||
result = spk_man->GetNewDestination(dest, error);
|
||||
} else {
|
||||
error = strprintf("Error: No addresses available.");
|
||||
}
|
||||
if (result) {
|
||||
SetAddressBook(dest, label, "receive");
|
||||
@ -4576,8 +4535,10 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C
|
||||
walletInstance->SetMaxVersion(FEATURE_LATEST);
|
||||
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))) {
|
||||
// Create new HD chain
|
||||
@ -4622,15 +4583,23 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C
|
||||
gArgs.ForceRemoveArg("mnemonic");
|
||||
gArgs.ForceRemoveArg("mnemonicpassphrase");
|
||||
}
|
||||
LOCK(walletInstance->cs_wallet);
|
||||
if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
walletInstance->SetupDescriptorScriptPubKeyMans();
|
||||
// SetupDescriptorScriptPubKeyMans already calls SetupGeneration for us so we don't need to call SetupGeneration separately
|
||||
}
|
||||
} // Otherwise, do not create a new HD chain
|
||||
|
||||
// Top up the keypool
|
||||
{
|
||||
LOCK(walletInstance->cs_wallet);
|
||||
if (auto spk_man = walletInstance->GetLegacyScriptPubKeyMan()) {
|
||||
if (spk_man->CanGenerateKeys() && !spk_man->TopUp()) {
|
||||
error = _("Unable to generate initial keys");
|
||||
return nullptr;
|
||||
if (!walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
// Legacy wallets need SetupGeneration here.
|
||||
if (auto spk_man = walletInstance->GetLegacyScriptPubKeyMan()) {
|
||||
if (spk_man->CanGenerateKeys() && !spk_man->TopUp()) {
|
||||
error = _("Unable to generate initial keys");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5455,6 +5424,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.
|
||||
if (m_internal_spk_managers == nullptr) return nullptr;
|
||||
@ -5469,7 +5441,7 @@ LegacyScriptPubKeyMan* CWallet::GetOrCreateLegacyScriptPubKeyMan()
|
||||
|
||||
void CWallet::SetupLegacyScriptPubKeyMan()
|
||||
{
|
||||
if (m_internal_spk_managers || m_external_spk_managers || !m_spk_managers.empty()) {
|
||||
if (m_internal_spk_managers || m_external_spk_managers || !m_spk_managers.empty() || IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -5541,3 +5513,152 @@ void CWallet::UpdateProgress(const std::string& title, int nProgress)
|
||||
{
|
||||
ShowProgress(title, nProgress);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (bool internal : {false, true}) {
|
||||
{ // OUTPUT_TYPE is only one: LEGACY
|
||||
auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, 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, internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CWallet::SetActiveScriptPubKeyMan(uint256 id, bool internal, bool memonly)
|
||||
{
|
||||
WalletLogPrintf("Setting spkMan to active: id = %s, type = %d, internal = %d\n", id.ToString(), static_cast<int>(OutputType::LEGACY), 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(internal);
|
||||
spk_mans = spk_man;
|
||||
|
||||
if (!memonly) {
|
||||
WalletBatch batch(GetDatabase());
|
||||
if (!batch.WriteActiveScriptPubKeyMan(id, internal)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing active ScriptPubKeyMan id failed");
|
||||
}
|
||||
}
|
||||
NotifyCanGetAddressesChanged();
|
||||
|
||||
}
|
||||
|
||||
bool CWallet::IsLegacy() const
|
||||
{
|
||||
if (m_internal_spk_managers == nullptr) return false;
|
||||
auto spk_man = dynamic_cast<LegacyScriptPubKeyMan*>(m_internal_spk_managers);
|
||||
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}) {
|
||||
{ // only one OutputType - LEGACY
|
||||
auto active_spk_man = GetScriptPubKeyMan(internal);
|
||||
if (active_spk_man && active_spk_man->GetID() == old_spk_man_id) {
|
||||
if (internal) {
|
||||
m_internal_spk_managers = nullptr;
|
||||
} else {
|
||||
m_external_spk_managers = nullptr;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -118,7 +118,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;
|
||||
@ -128,6 +129,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;
|
||||
@ -1316,6 +1318,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();
|
||||
@ -1388,6 +1393,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, 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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -31,6 +31,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"};
|
||||
@ -56,6 +58,10 @@ const std::string PRIVATESEND_SALT{"ps_salt"};
|
||||
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
|
||||
@ -221,6 +227,50 @@ bool WalletBatch::WriteGovernanceObject(const Governance::Object& obj)
|
||||
return WriteIC(std::make_pair(DBKeys::G_OBJECT, obj.GetHash()), obj, false);
|
||||
}
|
||||
|
||||
bool WalletBatch::WriteActiveScriptPubKeyMan(const uint256& id, bool internal)
|
||||
{
|
||||
std::string key = internal ? DBKeys::ACTIVEINTERNALSPK : DBKeys::ACTIVEEXTERNALSPK;
|
||||
return WriteIC(key, 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)), 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:
|
||||
@ -233,6 +283,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() {
|
||||
}
|
||||
@ -509,6 +564,107 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
||||
} else if (strType == DBKeys::OLD_KEY) {
|
||||
strErr = "Found unsupported 'wkey' record, try loading with version 0.17";
|
||||
return false;
|
||||
} else if (strType == DBKeys::ACTIVEEXTERNALSPK || strType == DBKeys::ACTIVEINTERNALSPK) {
|
||||
uint256 id;
|
||||
ssValue >> id;
|
||||
|
||||
bool internal = strType == DBKeys::ACTIVEINTERNALSPK;
|
||||
auto& spk_mans = internal ? wss.m_active_internal_spks : wss.m_active_external_spks;
|
||||
const OutputType type = OutputType::LEGACY;
|
||||
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) != 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 &&
|
||||
@ -613,6 +769,31 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
|
||||
}
|
||||
m_batch->CloseCursor();
|
||||
|
||||
// Set the active ScriptPubKeyMans
|
||||
for (auto spk_man : wss.m_active_external_spks) {
|
||||
pwallet->SetActiveScriptPubKeyMan(spk_man.second, /* internal */ false, /* memonly */ true);
|
||||
}
|
||||
for (auto spk_man : wss.m_active_internal_spks) {
|
||||
pwallet->SetActiveScriptPubKeyMan(spk_man.second, /* 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;
|
||||
|
||||
@ -633,7 +814,7 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
|
||||
wss.nWatchKeys, wss.nHDPubKeys, wss.nKeyMeta, wss.m_unknown_records);
|
||||
|
||||
// nTimeFirstKey is only reliable if all keys have metadata
|
||||
if ((wss.nKeys + wss.nCKeys + wss.nWatchKeys + wss.nHDPubKeys) != wss.nKeyMeta) {
|
||||
if (pwallet->IsLegacy() && (wss.nKeys + wss.nCKeys + wss.nWatchKeys + wss.nHDPubKeys) != 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>
|
||||
@ -58,6 +59,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_HDCHAIN;
|
||||
@ -83,6 +86,9 @@ extern const std::string PRIVATESEND_SALT;
|
||||
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
|
||||
@ -206,11 +212,19 @@ public:
|
||||
/** Write a CGovernanceObject to the database */
|
||||
bool WriteGovernanceObject(const Governance::Object& obj);
|
||||
|
||||
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(const uint256& id, bool internal);
|
||||
|
||||
DBErrors LoadWallet(CWallet* pwallet);
|
||||
DBErrors FindWalletTx(std::vector<uint256>& vTxHash, std::list<CWalletTx>& vWtx);
|
||||
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut);
|
||||
|
@ -6,6 +6,7 @@
|
||||
#define BITCOIN_WALLET_WALLETUTIL_H
|
||||
|
||||
#include <fs.h>
|
||||
#include <script/descriptor.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
@ -48,9 +49,45 @@ 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.
|
||||
fs::path GetWalletDir();
|
||||
|
||||
/** 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;
|
||||
|
||||
void DeserializeDescriptor(const std::string& str)
|
||||
{
|
||||
std::string error;
|
||||
FlatSigningProvider keys;
|
||||
descriptor = Parse(str, keys, error, true);
|
||||
if (!descriptor) {
|
||||
throw std::ios_base::failure("Invalid descriptor: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
SERIALIZE_METHODS(WalletDescriptor, obj)
|
||||
{
|
||||
std::string descriptor_str;
|
||||
SER_WRITE(obj, descriptor_str = obj.descriptor->ToString());
|
||||
READWRITE(descriptor_str, obj.creation_time, obj.next_index, obj.range_start, obj.range_end);
|
||||
SER_READ(obj, obj.DeserializeDescriptor(descriptor_str));
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -82,7 +82,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']
|
||||
@ -95,17 +95,17 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
||||
self.sync_blocks()
|
||||
|
||||
# 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"], "")["address"]
|
||||
address_18075 = wallet.rpc.addmultisigaddress(1, ["0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52", "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073"], "")["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']
|
||||
@ -114,28 +114,28 @@ 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
|
||||
|
||||
# w1_v20: regular wallet, created with v20.0
|
||||
node_v20.createwallet(wallet_name="w1_v20")
|
||||
node_v20.rpc.createwallet(wallet_name="w1_v20")
|
||||
wallet = node_v20.get_wallet_rpc("w1_v20")
|
||||
info = wallet.getwalletinfo()
|
||||
assert info['private_keys_enabled']
|
||||
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
|
||||
@ -143,21 +143,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']
|
||||
@ -331,7 +331,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()
|
||||
v17_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):
|
||||
@ -63,11 +68,14 @@ 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)['address']
|
||||
assert_equal(legacy_addr, node0.addmultisigaddress(2, keys, '')['address'])
|
||||
assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '')['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:
|
||||
@ -82,6 +90,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() for _ in range(2)]
|
||||
assert_raises_rpc_error(-5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses))
|
||||
@ -109,6 +119,16 @@ 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:
|
||||
path = os.path.join(self.options.tmpdir, "node1", "regtest", "wallets", "wmulti")
|
||||
if e.error['code'] == -18 and "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path) 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))
|
||||
@ -121,7 +141,7 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
||||
assert_equal(desc, msig['descriptor'])
|
||||
|
||||
# compare against addmultisigaddress
|
||||
msigw = node1.addmultisigaddress(self.nsigs, self.pub, None)
|
||||
msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None)
|
||||
maddw = msigw["address"]
|
||||
mredeemw = msigw["redeemScript"]
|
||||
assert_equal(desc, drop_origins(msigw['descriptor']))
|
||||
@ -168,6 +188,8 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
|
||||
txinfo = node0.getrawtransaction(tx, True, blk)
|
||||
self.log.info("n/m=%d/%d size=%d" % (self.nsigs, self.nkeys, txinfo["size"]))
|
||||
|
||||
wmulti.unloadwallet()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
RpcCreateMultiSigTest().main()
|
||||
|
@ -49,16 +49,26 @@ class PSBTTest(BitcoinTestFramework):
|
||||
# Locks are ignored for manually selected inputs
|
||||
self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():1}, 0)
|
||||
|
||||
# Create p2sh and p2pkh 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])['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])['address']
|
||||
|
||||
if not self.options.descriptors:
|
||||
wmulti.importaddress(p2sh)
|
||||
|
||||
p2pkh = self.nodes[1].getnewaddress()
|
||||
|
||||
# fund those addresses
|
||||
rawtx = self.nodes[0].createrawtransaction([], {p2sh:10, p2pkh:10})
|
||||
rawtx = self.nodes[0].fundrawtransaction(rawtx, {"changePosition":2})
|
||||
rawtx = self.nodes[0].fundrawtransaction(rawtx, {"changePosition":1})
|
||||
signed_tx = self.nodes[0].signrawtransactionwithwallet(rawtx['hex'])['hex']
|
||||
txid = self.nodes[0].sendrawtransaction(signed_tx)
|
||||
self.nodes[0].generate(6)
|
||||
@ -86,23 +96,26 @@ class PSBTTest(BitcoinTestFramework):
|
||||
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
|
||||
|
||||
# feeRate of 0.1 DASH / KB produces a total fee slightly below -maxtxfee (~0.06650000):
|
||||
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2pkh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1}, False)
|
||||
assert_greater_than(res["fee"], 0.06)
|
||||
assert_greater_than(0.07, res["fee"])
|
||||
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 0.1}, False)
|
||||
assert_greater_than(res["fee"], 0.03)
|
||||
assert_greater_than(0.04, res["fee"])
|
||||
decoded_psbt = self.nodes[0].decodepsbt(res['psbt'])
|
||||
for psbt_in in decoded_psbt["inputs"]:
|
||||
assert "bip32_derivs" not in psbt_in
|
||||
|
||||
# feeRate of 10 DASH / KB produces a total fee well above -maxtxfee
|
||||
# previously this was silently capped at -maxtxfee
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10})
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 10})
|
||||
|
||||
# partially sign multisig things with node 1
|
||||
psbtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2sh_pos}], {self.nodes[1].getnewaddress():9.99})['psbt']
|
||||
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2sh_pos}], outputs={self.nodes[1].getnewaddress():9.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)
|
||||
@ -212,7 +225,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)
|
||||
|
@ -14,6 +14,7 @@ import random
|
||||
import unittest
|
||||
|
||||
from test_framework.crypto import secp256k1
|
||||
from .address import byte_to_base58
|
||||
|
||||
# Order of the secp256k1 curve
|
||||
ORDER = secp256k1.GE.ORDER
|
||||
@ -184,6 +185,17 @@ class ECKey:
|
||||
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)
|
||||
|
||||
def compute_xonly_pubkey(key):
|
||||
"""Compute an x-only (32 byte) public key from a (32 byte) private key.
|
||||
|
||||
|
@ -219,6 +219,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||
parser.add_argument("--randomseed", type=int,
|
||||
help="set a random seed for deterministically reproducing a previous test run")
|
||||
parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts')
|
||||
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()
|
||||
@ -424,7 +426,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||
"""Override this method to customize test node setup"""
|
||||
|
||||
"""If this method is updated - backport changes to DashTestFramework.setup_nodes"""
|
||||
extra_args = None
|
||||
extra_args = [[]] * self.num_nodes
|
||||
if hasattr(self, "extra_args"):
|
||||
extra_args = self.extra_args
|
||||
self.add_nodes(self.num_nodes, extra_args)
|
||||
@ -455,7 +457,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||
if wallet_name is not False:
|
||||
n = self.nodes[i]
|
||||
if wallet_name is not None:
|
||||
n.createwallet(wallet_name=wallet_name, load_on_startup=True)
|
||||
n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True)
|
||||
n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase')
|
||||
|
||||
def run_test(self):
|
||||
@ -526,6 +528,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||
use_cli=self.options.usecli,
|
||||
start_perf=self.options.perf,
|
||||
use_valgrind=self.options.valgrind,
|
||||
descriptors=self.options.descriptors,
|
||||
)
|
||||
self.nodes.append(test_node_i)
|
||||
if not test_node_i.version_is_at_least(160000):
|
||||
@ -878,6 +881,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||
mocktime=self.mocktime,
|
||||
coverage_dir=None,
|
||||
cwd=self.options.tmpdir,
|
||||
descriptors=self.options.descriptors,
|
||||
))
|
||||
self.start_node(CACHE_NODE_ID)
|
||||
cache_node = self.nodes[CACHE_NODE_ID]
|
||||
@ -1422,7 +1426,7 @@ class DashTestFramework(BitcoinTestFramework):
|
||||
force_finish_mnsync(self.nodes[mnidx])
|
||||
|
||||
def setup_nodes(self):
|
||||
extra_args = None
|
||||
extra_args = [[]] * self.num_nodes
|
||||
if hasattr(self, "extra_args"):
|
||||
extra_args = self.extra_args
|
||||
self.log.info("Creating and starting controller node")
|
||||
|
@ -22,6 +22,7 @@ import sys
|
||||
import collections
|
||||
|
||||
from .authproxy import JSONRPCException
|
||||
from .descriptors import descsum_create
|
||||
from .messages import MY_SUBVERSION
|
||||
from .util import (
|
||||
MAX_NODES,
|
||||
@ -63,7 +64,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, extra_args_from_options, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, mocktime, 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, extra_args_from_options, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, mocktime, 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
|
||||
@ -82,6 +83,7 @@ class TestNode():
|
||||
self.coverage_dir = coverage_dir
|
||||
self.cwd = cwd
|
||||
self.mocktime = mocktime
|
||||
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.
|
||||
@ -187,10 +189,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."""
|
||||
@ -316,11 +318,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 version_is_at_least(self, ver):
|
||||
return self.version is None or self.version >= ver
|
||||
@ -660,3 +662,118 @@ class TestNodeCLI():
|
||||
return json.loads(cli_stdout, parse_float=decimal.Decimal)
|
||||
except (json.JSONDecodeError, decimal.InvalidOperation):
|
||||
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, load_on_startup=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 load_on_startup is None:
|
||||
load_on_startup = 'null'
|
||||
if descriptors is None:
|
||||
descriptors = self.descriptors
|
||||
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup)
|
||||
|
||||
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):
|
||||
wallet_info = self.getwalletinfo()
|
||||
if self.is_cli:
|
||||
if label is None:
|
||||
label = 'null'
|
||||
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
|
||||
return self.__getattr__('addmultisigaddress')(nrequired, keys, label)
|
||||
cms = self.createmultisig(nrequired, keys)
|
||||
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'])
|
||||
|
@ -9,6 +9,10 @@ from test_framework.address import (
|
||||
key_to_p2pkh,
|
||||
script_to_p2sh,
|
||||
)
|
||||
from test_framework.key import (
|
||||
bytes_to_wif,
|
||||
ECKey,
|
||||
)
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
OP_2,
|
||||
@ -46,6 +50,20 @@ def get_key(node):
|
||||
p2pkh_script=CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]).hex(),
|
||||
p2pkh_addr=key_to_p2pkh(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))
|
||||
|
||||
def get_multisig(node):
|
||||
"""Generate a fresh 2-of-3 multisig on node
|
||||
|
||||
|
@ -95,6 +95,7 @@ BASE_SCRIPTS = [
|
||||
'feature_dip3_deterministicmns.py', # NOTE: needs dash_hash to pass
|
||||
'feature_llmq_data_recovery.py',
|
||||
'wallet_hd.py',
|
||||
'wallet_hd.py --descriptors',
|
||||
'wallet_backup.py',
|
||||
# vv Tests less than 5m vv
|
||||
'mining_getblocktemplate_longpoll.py', # FIXME: "socket.error: [Errno 54] Connection reset by peer" on my Mac, same as https://github.com/bitcoin/bitcoin/issues/6651
|
||||
@ -107,7 +108,9 @@ BASE_SCRIPTS = [
|
||||
# vv Tests less than 2m vv
|
||||
'p2p_instantsend.py',
|
||||
'wallet_basic.py',
|
||||
'wallet_basic.py --descriptors',
|
||||
'wallet_labels.py',
|
||||
'wallet_labels.py --descriptors',
|
||||
'p2p_timeouts.py',
|
||||
'feature_bip68_sequence.py',
|
||||
'mempool_updatefromblock.py',
|
||||
@ -144,6 +147,7 @@ BASE_SCRIPTS = [
|
||||
# vv Tests less than 30s vv
|
||||
'rpc_quorum.py',
|
||||
'wallet_keypool_topup.py',
|
||||
'wallet_keypool_topup.py --descriptors',
|
||||
'feature_fee_estimation.py',
|
||||
'interface_zmq_dash.py',
|
||||
'interface_zmq.py',
|
||||
@ -158,6 +162,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',
|
||||
@ -169,6 +174,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',
|
||||
@ -189,6 +195,8 @@ BASE_SCRIPTS = [
|
||||
'rpc_net.py',
|
||||
'wallet_keypool.py',
|
||||
'wallet_keypool_hd.py',
|
||||
'wallet_keypool_hd.py --descriptors',
|
||||
'wallet_descriptor.py',
|
||||
'p2p_nobloomfilter_messages.py',
|
||||
'p2p_filter.py',
|
||||
'p2p_blocksonly.py',
|
||||
@ -211,6 +219,7 @@ BASE_SCRIPTS = [
|
||||
'mempool_packages.py',
|
||||
'mempool_package_onemore.py',
|
||||
'rpc_createmultisig.py',
|
||||
'rpc_createmultisig.py --descriptors',
|
||||
'rpc_packages.py',
|
||||
'feature_versionbits_warning.py',
|
||||
'rpc_preciousblock.py',
|
||||
@ -226,6 +235,7 @@ BASE_SCRIPTS = [
|
||||
'wallet_import_rescan.py',
|
||||
'wallet_import_with_label.py',
|
||||
'wallet_upgradewallet.py',
|
||||
'wallet_importdescriptors.py',
|
||||
'wallet_mnemonicbits.py',
|
||||
'rpc_bind.py --ipv4',
|
||||
'rpc_bind.py --ipv6',
|
||||
@ -239,6 +249,7 @@ BASE_SCRIPTS = [
|
||||
'feature_sporks.py',
|
||||
'rpc_getblockstats.py',
|
||||
'wallet_encryption.py',
|
||||
'wallet_encryption.py --descriptors',
|
||||
'wallet_upgradetohd.py',
|
||||
'feature_dersig.py',
|
||||
'feature_cltv.py',
|
||||
|
@ -126,7 +126,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
|
||||
@ -242,31 +242,32 @@ class AvoidReuseTest(BitcoinTestFramework):
|
||||
# getbalances should show no used, 5 btc trusted
|
||||
assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5})
|
||||
|
||||
self.nodes[0].sendtoaddress(fundaddr, 10)
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
if not self.options.descriptors:
|
||||
self.nodes[0].sendtoaddress(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):
|
||||
'''
|
||||
|
@ -20,10 +20,15 @@ from test_framework.wallet_util import test_address
|
||||
class WalletTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 4
|
||||
self.extra_args = [[
|
||||
"-acceptnonstdtxn=1",
|
||||
'-usehd={:d}'.format(i%2==0),
|
||||
] for i in range(self.num_nodes)]
|
||||
if self.options.descriptors:
|
||||
self.extra_args = [[
|
||||
"-acceptnonstdtxn=1"
|
||||
] for i in range(self.num_nodes)]
|
||||
else:
|
||||
self.extra_args = [[
|
||||
"-acceptnonstdtxn=1",
|
||||
'-usehd={:d}'.format(i%2==0)
|
||||
] for i in range(self.num_nodes)]
|
||||
self.setup_clean_chain = True
|
||||
self.supports_cli = False
|
||||
|
||||
@ -46,6 +51,7 @@ class WalletTest(BitcoinTestFramework):
|
||||
return curr_balance
|
||||
|
||||
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)
|
||||
@ -229,7 +235,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('100'), fee_per_byte, count_bytes(self.nodes[2].gettransaction(txid)['hex']))
|
||||
|
||||
self.start_node(3)
|
||||
self.start_node(3, self.nodes[3].extra_args)
|
||||
self.connect_nodes(0, 3)
|
||||
# Sendmany with explicit fee (DASH/kB)
|
||||
# Throw if no conf_target provided
|
||||
@ -378,125 +384,127 @@ 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()
|
||||
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(-4, "Private key for address %s is not known" % temp_address, 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()
|
||||
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 Dash address
|
||||
assert_raises_rpc_error(-5, "Invalid Dash 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(-4, "Private key for address %s is not known" % temp_address, self.nodes[0].dumpprivkey, temp_address)
|
||||
|
||||
# This will raise an exception for attempting to set a label for an invalid Dash address
|
||||
assert_raises_rpc_error(-5, "Invalid Dash address", self.nodes[0].setlabel, "invalid address", "label")
|
||||
# This will raise an exception for attempting to get the private key of an invalid Dash address
|
||||
assert_raises_rpc_error(-5, "Invalid Dash address", self.nodes[0].dumpprivkey, "invalid")
|
||||
|
||||
# This will raise an exception for importing an invalid address
|
||||
assert_raises_rpc_error(-5, "Invalid Dash address or script", self.nodes[0].importaddress, "invalid")
|
||||
# This will raise an exception for attempting to set a label for an invalid Dash address
|
||||
assert_raises_rpc_error(-5, "Invalid Dash 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 Dash 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")
|
||||
|
||||
# send with explicit dash/kb fee
|
||||
self.log.info("test explicit fee (sendtoaddress as dash/kb)")
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
prebalance = self.nodes[2].getbalance()
|
||||
assert prebalance > 2
|
||||
address = self.nodes[1].getnewaddress()
|
||||
# Throw if no conf_target provided
|
||||
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
estimate_mode='dash/Kb')
|
||||
# Throw if negative feerate
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=-1,
|
||||
estimate_mode='dash/kb')
|
||||
txid = self.nodes[2].sendtoaddress(
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=0.00002500,
|
||||
estimate_mode='dash/kb',
|
||||
)
|
||||
tx_size = count_bytes(self.nodes[2].gettransaction(txid)['hex'])
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
postbalance = self.nodes[2].getbalance()
|
||||
fee = prebalance - postbalance - Decimal('1')
|
||||
assert_fee_amount(fee, tx_size, Decimal('0.00002500'))
|
||||
# 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])
|
||||
|
||||
# send with explicit duff/b fee
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.log.info("test explicit fee (sendtoaddress as duff/b)")
|
||||
self.nodes[0].generate(1)
|
||||
prebalance = self.nodes[2].getbalance()
|
||||
assert prebalance > 2
|
||||
address = self.nodes[1].getnewaddress()
|
||||
# Throw if no conf_target provided
|
||||
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
estimate_mode='duff/b')
|
||||
# Throw if negative feerate
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=-1,
|
||||
estimate_mode='duff/b')
|
||||
txid = self.nodes[2].sendtoaddress(
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=2,
|
||||
estimate_mode='duff/B',
|
||||
)
|
||||
tx_size = count_bytes(self.nodes[2].gettransaction(txid)['hex'])
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
postbalance = self.nodes[2].getbalance()
|
||||
fee = prebalance - postbalance - Decimal('1')
|
||||
assert_fee_amount(fee, tx_size, Decimal('0.00002000'))
|
||||
# send with explicit dash/kb fee
|
||||
self.log.info("test explicit fee (sendtoaddress as dash/kb)")
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
prebalance = self.nodes[2].getbalance()
|
||||
assert prebalance > 2
|
||||
address = self.nodes[1].getnewaddress()
|
||||
# Throw if no conf_target provided
|
||||
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
estimate_mode='dash/Kb')
|
||||
# Throw if negative feerate
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=-1,
|
||||
estimate_mode='dash/kb')
|
||||
txid = self.nodes[2].sendtoaddress(
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=0.00002500,
|
||||
estimate_mode='dash/kb',
|
||||
)
|
||||
tx_size = count_bytes(self.nodes[2].gettransaction(txid)['hex'])
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
postbalance = self.nodes[2].getbalance()
|
||||
fee = prebalance - postbalance - Decimal('1')
|
||||
assert_fee_amount(fee, tx_size, Decimal('0.00002500'))
|
||||
|
||||
# 2. Import address from node2 to node1
|
||||
self.nodes[1].importaddress(address_to_import)
|
||||
# send with explicit duff/b fee
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.log.info("test explicit fee (sendtoaddress as duff/b)")
|
||||
self.nodes[0].generate(1)
|
||||
prebalance = self.nodes[2].getbalance()
|
||||
assert prebalance > 2
|
||||
address = self.nodes[1].getnewaddress()
|
||||
# Throw if no conf_target provided
|
||||
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
estimate_mode='duff/b')
|
||||
# Throw if negative feerate
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[2].sendtoaddress,
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=-1,
|
||||
estimate_mode='duff/b')
|
||||
txid = self.nodes[2].sendtoaddress(
|
||||
address=address,
|
||||
amount=1.0,
|
||||
conf_target=2,
|
||||
estimate_mode='duff/B',
|
||||
)
|
||||
tx_size = count_bytes(self.nodes[2].gettransaction(txid)['hex'])
|
||||
self.sync_all(self.nodes[0:3])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all(self.nodes[0:3])
|
||||
postbalance = self.nodes[2].getbalance()
|
||||
fee = prebalance - postbalance - Decimal('1')
|
||||
assert_fee_amount(fee, tx_size, Decimal('0.00002000'))
|
||||
|
||||
# 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()
|
||||
@ -560,7 +568,7 @@ class WalletTest(BitcoinTestFramework):
|
||||
self.nodes[0].generate(1)
|
||||
node0_balance = self.nodes[0].getbalance()
|
||||
# Split into two chains
|
||||
rawtx = self.nodes[0].createrawtransaction([{"txid": singletxid, "vout": 0}], {chain_addrs[0]: node0_balance / 2 - Decimal('0.01'), chain_addrs[1]: node0_balance / 2 - Decimal('0.01')})
|
||||
rawtx = self.nodes[0].createrawtransaction([{"txid": singletxid, "vout": 0}], {chain_addrs[0]: node0_balance // 2 - Decimal('0.01'), chain_addrs[1]: node0_balance // 2 - Decimal('0.01')})
|
||||
signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx)
|
||||
singletxid = self.nodes[0].sendrawtransaction(hexstring=signedtx["hex"], maxfeerate=0)
|
||||
self.nodes[0].generate(1)
|
||||
@ -586,7 +594,9 @@ class WalletTest(BitcoinTestFramework):
|
||||
|
||||
# Try with walletrejectlongchains
|
||||
# Double chain limit but require combining inputs, so we pass SelectCoinsMinConf
|
||||
self.restart_node(0, extra_args=["-walletrejectlongchains", "-limitancestorcount=" + str(2 * chainlimit)])
|
||||
self.stop_node(0)
|
||||
extra_args = ["-walletrejectlongchains", "-limitancestorcount=" + str(2 * chainlimit)]
|
||||
self.start_node(0, extra_args=extra_args)
|
||||
|
||||
# wait until the wallet has submitted all transactions to the mempool
|
||||
self.wait_until(lambda: len(self.nodes[0].getrawmempool()) == chainlimit * 2)
|
||||
|
127
test/functional/wallet_descriptor.py
Executable file
127
test/functional/wallet_descriptor.py
Executable file
@ -0,0 +1,127 @@
|
||||
#!/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']]
|
||||
self.wallet_names = []
|
||||
|
||||
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)
|
||||
|
||||
# A descriptor wallet should have 100 addresses * 1 type = 100 keys
|
||||
self.log.info("Checking wallet info")
|
||||
wallet_info = self.nodes[0].getwalletinfo()
|
||||
assert_equal(wallet_info['keypoolsize'], 100)
|
||||
assert_equal(wallet_info['keypoolsize_hd_internal'], 100)
|
||||
assert 'keypoololdest' not in wallet_info
|
||||
|
||||
# Check that getnewaddress works
|
||||
self.log.info("Test that getnewaddress and getrawchangeaddress work")
|
||||
addr = self.nodes[0].getnewaddress("")
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('pkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/44\'/1\'/0\'/0/0')
|
||||
|
||||
# Check that getrawchangeaddress works
|
||||
addr = self.nodes[0].getrawchangeaddress()
|
||||
addr_info = self.nodes[0].getaddressinfo(addr)
|
||||
assert addr_info['desc'].startswith('pkh(')
|
||||
assert_equal(addr_info['hdkeypath'], 'm/44\'/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')
|
||||
# TODO: there is not RPC `sethdseed`
|
||||
# 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": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
|
||||
"timestamp": "now",
|
||||
"range": [0,10],
|
||||
"active": True
|
||||
}])
|
||||
send_wrpc.walletlock()
|
||||
# Exhaust keypool of 100
|
||||
for i in range(0, 100):
|
||||
# keypool should be exhaused by bech32 addresses, but in dash case keypool is not exhausted
|
||||
send_wrpc.getnewaddress()
|
||||
# This should now error
|
||||
# this check is disabled, see comment above above
|
||||
# assert_raises_rpc_error(-12, "Keypool ran out, please call keypoolrefill first", send_wrpc.getnewaddress, '')
|
||||
|
||||
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,
|
||||
@ -28,10 +27,10 @@ class WalletEncryptionTest(BitcoinTestFramework):
|
||||
passphrase2 = "SecondWalletPassphrase"
|
||||
|
||||
# Make sure the wallet isn't encrypted first
|
||||
msg = "test message"
|
||||
address = self.nodes[0].getnewaddress()
|
||||
privkey = self.nodes[0].dumpprivkey(address)
|
||||
assert_equal(privkey[:1], "c")
|
||||
assert_equal(len(privkey), 52)
|
||||
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')
|
||||
|
||||
@ -40,33 +39,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(4) # Wait a little bit longer to make sure wallet gets locked
|
||||
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
|
||||
|
@ -35,8 +35,8 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
self.connect_nodes(0, 1)
|
||||
|
||||
# Make sure we use hd, keep chainid
|
||||
chainid = self.nodes[1].getwalletinfo()['hdchainid']
|
||||
assert_equal(len(chainid), 64)
|
||||
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()
|
||||
@ -44,8 +44,9 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
assert_equal(change_addrV["hdkeypath"], "m/44'/1'/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 = 'yLU9vxiAWUdiKKxn6EazLDFq9WXrK2T7RP'
|
||||
non_hd_key = 'cVCzrzfxMhUMxV34UhTmdmntAqHvosAuNo2KUZsiHZSKLm73g35o'
|
||||
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"))
|
||||
@ -56,11 +57,11 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
self.nodes[0].generate(COINBASE_MATURITY + 1)
|
||||
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/44'/1'/0'/0/"+str(i))
|
||||
assert_equal(hd_info["hdchainid"], chainid)
|
||||
assert_equal(hd_info["hdkeypath"], "m/44'/1'/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)
|
||||
@ -84,17 +85,17 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
shutil.rmtree(os.path.join(self.nodes[1].datadir, self.chain, "llmq"))
|
||||
shutil.copyfile(
|
||||
os.path.join(self.nodes[1].datadir, "hd.bak"),
|
||||
os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename),
|
||||
os.path.join(self.nodes[1].datadir, self.chain, "wallets", self.default_wallet_name, self.wallet_data_filename),
|
||||
)
|
||||
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/44'/1'/0'/0/"+str(i))
|
||||
assert_equal(hd_info_2["hdchainid"], chainid)
|
||||
assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
|
||||
assert_equal(hd_add, hd_add_2)
|
||||
self.connect_nodes(0, 1)
|
||||
self.sync_all()
|
||||
|
419
test/functional/wallet_importdescriptors.py
Executable file
419
test/functional/wallet_importdescriptors.py
Executable file
@ -0,0 +1,419 @@
|
||||
#!/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 = [[],
|
||||
["-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-P2PKH descriptor
|
||||
key = get_generate_key()
|
||||
self.log.info("Should not import a p2sh-p2pkh descriptor without checksum")
|
||||
self.test_importdesc({"desc": "sh(pkh(" + key.pubkey + "))",
|
||||
"timestamp": "now"
|
||||
},
|
||||
success=False,
|
||||
error_code=-5,
|
||||
error_message="Missing checksum")
|
||||
|
||||
self.log.info("Should not import a p2sh-p2pkh descriptor that has range specified")
|
||||
self.test_importdesc({"desc": descsum_create("sh(pkh(" + 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-p2pkh descriptor and have it set to active")
|
||||
self.test_importdesc({"desc": descsum_create("sh(pkh(" + 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-p2pkh descriptor")
|
||||
self.test_importdesc({"desc": descsum_create("sh(pkh(" + key.pubkey + "))"),
|
||||
"timestamp": "now",
|
||||
"active": False,
|
||||
},
|
||||
success=True)
|
||||
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||
|
||||
# # 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 = "tpubDAXcJ7s7ZwicqjprRaEWdPoHKrCS215qxGYxpusRLLmJuT69ZSicuGdSfyvyKpvUNYBW1s2U3NSrT6vrCYB9e6nZUEvrqnwXPF8ArTCRXMY"
|
||||
addresses = ["yUxX4qnzWntXhEGrYB92v7ez4EZBnUjB1y", "yRhTPsPd2qYgYbFFCqY2nuPHJQBjTnMQxg"] # hdkeypath=m/0'/0'/0' and 1'
|
||||
desc = "sh(pkh(" + 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(pkh(" + 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 = [
|
||||
'yUxX4qnzWntXhEGrYB92v7ez4EZBnUjB1y', # m/0'/0'/0
|
||||
'yRhTPsPd2qYgYbFFCqY2nuPHJQBjTnMQxg', # m/0'/0'/1
|
||||
'yUyn3UV9rBdWfw6yJJ6eAoKuzDJ8RVLP1o', # m/0'/0'/2
|
||||
'yi8GEkfLBgK85wGmBFsMFdSbEvPPNCSnVx', # m/0'/0'/3
|
||||
'yYB4whdY8APWoCez6ryNdMBrrDjwzFbqMi', # m/0'/0'/4
|
||||
]
|
||||
|
||||
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)
|
||||
for i, expected_addr in enumerate(addresses):
|
||||
received_addr = w1.getnewaddress('')
|
||||
assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress)
|
||||
assert_equal(received_addr, expected_addr)
|
||||
|
||||
pkh_addr = received_addr
|
||||
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) # 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 )
|
||||
|
||||
# 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)
|
||||
|
||||
# # Test importing a descriptor containing a WIF private key
|
||||
wif_priv = "cTT3BvHnd51YJf8fkdr2XvZTQRRUZruWhRvRyQY1raVFg5Lvam2A"
|
||||
address = "ySWABbcNKyHUgBb1ffhpuETuis9jsdR3aq"
|
||||
desc = "pkh(" + 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":"sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/0h/0h/*))#f5nqn4ax",
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_priv)
|
||||
self.test_importdesc({"desc":"sh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/1h/0h/*))#m4e4s5de",
|
||||
"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()
|
||||
assert_equal(addr, '8vEwYGKBMP3F2juEE36nNqh1uYpBv9QFyB') # Derived at m/84'/0'/0'/0
|
||||
change_addr = wmulti_priv.getrawchangeaddress()
|
||||
assert_equal(change_addr, '91WxMwg2NHD1PwHChhbAkeCN6nQ8ikdLEx')
|
||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
|
||||
txid = w0.sendtoaddress(addr, 10)
|
||||
self.nodes[0].generate(6)
|
||||
wmulti_priv.sendtoaddress(w0.getnewaddress(), 8)
|
||||
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":"sh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))#x75vpsak",
|
||||
"active": True,
|
||||
"range": 1000,
|
||||
"next_index": 0,
|
||||
"timestamp": "now"},
|
||||
success=True,
|
||||
wallet=wmulti_pub)
|
||||
self.test_importdesc({"desc":"sh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))#v0t48ucu",
|
||||
"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()
|
||||
assert_equal(addr, '91cA4fLGaDCr6b9W2c5j1ph9PDpq9WbEhk') # Derived at m/84'/0'/0'/1
|
||||
change_addr = wmulti_pub.getrawchangeaddress()
|
||||
assert_equal(change_addr, '91WxMwg2NHD1PwHChhbAkeCN6nQ8ikdLEx')
|
||||
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("sh(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("sh(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("sh(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("sh(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")
|
||||
|
||||
# we have only one type of descriptors so we can't trigger this warning
|
||||
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()
|
@ -28,16 +28,52 @@ 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['hdchainid'] == wallet_info_old['hdchainid']
|
||||
if not self.options.descriptors:
|
||||
assert addr_before_encrypting_data['hdchainid'] == wallet_info_old['hdchainid']
|
||||
|
||||
# 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": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1h/*h)#a0nyvl0k",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"desc": "sh(pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/2h/*h))#lmeu2axg",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True
|
||||
},
|
||||
{
|
||||
"desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/4h/*h)#l3crwaus",
|
||||
"timestamp": "now",
|
||||
"range": [0,0],
|
||||
"active": True,
|
||||
"internal": True
|
||||
},
|
||||
{
|
||||
"desc": "sh(pkh(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['hdchainid'] == wallet_info['hdchainid']
|
||||
assert addr_data['hdchainid'] == wallet_info['hdchainid']
|
||||
|
||||
# TODO: enable this assert after bitcoin#17681 is backported
|
||||
# assert addr_before_encrypting_data['hdmasterfingerprint'] != addr_data['hdmasterfingerprint']
|
||||
if not self.options.descriptors:
|
||||
assert addr_data['hdchainid'] == wallet_info['hdchainid']
|
||||
|
||||
try:
|
||||
addr = nodes[0].getnewaddress()
|
||||
@ -50,8 +86,13 @@ 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:
|
||||
# this counters are zero, bitcoin have here 6 * 3 (3 different types)
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 6)
|
||||
assert_equal(wi['keypoolsize'], 6)
|
||||
else:
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 6)
|
||||
assert_equal(wi['keypoolsize'], 6)
|
||||
|
||||
# drain the internal keys
|
||||
nodes[0].getrawchangeaddress()
|
||||
@ -98,11 +139,16 @@ 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:
|
||||
# dash has only 1 type of output addresses
|
||||
assert_equal(wi['keypoolsize_hd_internal'], 100)
|
||||
assert_equal(wi['keypoolsize'], 100)
|
||||
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
|
||||
@ -110,8 +156,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)
|
||||
|
||||
@ -141,7 +190,7 @@ class KeyPoolTest(BitcoinTestFramework):
|
||||
# create a transaction without change at the maximum fee rate, such that the output is still spendable:
|
||||
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00008824})
|
||||
assert_equal("psbt" in res, True)
|
||||
assert_equal(res["fee"], Decimal("0.00001685"))
|
||||
assert_equal(res["fee"], Decimal("0.00001694"))
|
||||
|
||||
# creating a 10,000 sat transaction with a manual change address should be possible
|
||||
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.000010, "changeAddress": addr.pop()})
|
||||
|
@ -118,15 +118,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 _ 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(COINBASE_MATURITY + 1)
|
||||
if not self.options.descriptors:
|
||||
for label in labels:
|
||||
addresses = []
|
||||
for _ 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(COINBASE_MATURITY + 1)
|
||||
|
||||
# Check that setlabel can change the label of an address from a
|
||||
# different label.
|
||||
|
Loading…
Reference in New Issue
Block a user