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:
Andrew Chow 2019-07-16 13:34:35 -04:00 committed by Konstantin Akimov
parent 4064334732
commit f293c046f4
No known key found for this signature in database
GPG Key ID: 2176C4A5D01EA524
38 changed files with 2676 additions and 340 deletions

View File

@ -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;

View File

@ -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;

View File

@ -77,3 +77,8 @@ bool CreateWalletDialog::isMakeBlankWalletChecked() const
{
return ui->blank_wallet_checkbox->isChecked();
}
bool CreateWalletDialog::isDescriptorWalletChecked() const
{
return ui->descriptor_checkbox->isChecked();
}

View File

@ -27,6 +27,7 @@ public:
bool isEncryptWalletChecked() const;
bool isDisablePrivateKeysChecked() const;
bool isMakeBlankWalletChecked() const;
bool isDescriptorWalletChecked() const;
private:
Ui::CreateWalletDialog *ui;

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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);

View File

@ -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"},

View File

@ -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; }
};
////////////////////////////////////////////////////////////////////////////

View File

@ -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;

View File

@ -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));

View File

@ -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;
}

View File

@ -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"} },

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
};
/**

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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.

View File

@ -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")

View File

@ -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'])

View File

@ -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

View File

@ -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',

View File

@ -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):
'''

View File

@ -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)

View 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 ()

View File

@ -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

View File

@ -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()

View 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()

View File

@ -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()})

View File

@ -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.