Skip to content

Commit 891068a

Browse files
pablomartin4btcBushstar
authored andcommitted
qt: Add regression test for signable address filter
Test that CanSignMessageRole correctly identifies only spendable PKHash addresses as signable — excluding watch-only PKHash addresses (no private key) and bech32 addresses (not PKHash). Also verifies that AddressBookPage::AddressFilter::Signable shows exactly those addresses. Previously the filter used pk_hash_only which only checked address type, allowing watch-only addresses through.
1 parent 2187f2b commit 891068a

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

src/qt/test/addressbooktests.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include <key.h>
2222
#include <key_io.h>
23+
#include <script/descriptor.h>
2324
#include <wallet/wallet.h>
2425
#include <wallet/test/util.h>
2526
#include <walletinitinterface.h>
@@ -38,6 +39,7 @@ using wallet::CreateMockableWalletDatabase;
3839
using wallet::RemoveWallet;
3940
using wallet::WALLET_FLAG_DESCRIPTORS;
4041
using wallet::WalletContext;
42+
using wallet::WalletDescriptor;
4143

4244
namespace
4345
{
@@ -210,6 +212,86 @@ void TestAddAddressesToSendBook(interfaces::Node& node)
210212
QCOMPARE(table_view->model()->rowCount(), 3);
211213
}
212214

215+
/**
216+
* Test that CanSignMessageRole correctly filters the sign-message address picker:
217+
* - watch-only PKHash addresses (no private key) → not signable
218+
* - bech32 addresses (not PKHash) → not signable
219+
* - spendable PKHash addresses (private key imported) → signable
220+
* Also verifies AddressBookPage::AddressFilter::Signable shows only signable addresses.
221+
*/
222+
void TestSignableAddressFilter(interfaces::Node& node)
223+
{
224+
TestChain100Setup test;
225+
auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args));
226+
test.m_node.wallet_loader = wallet_loader.get();
227+
node.setContext(&test.m_node);
228+
229+
const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
230+
node.context()->chain.get(), "", CreateMockableWalletDatabase());
231+
232+
{
233+
LOCK(wallet->cs_wallet);
234+
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
235+
wallet->SetupDescriptorScriptPubKeyMans();
236+
237+
// (a) Watch-only PKHash: address in book but no private key → not signable
238+
const PKHash watchonly_dest{GenerateRandomKey().GetPubKey()};
239+
wallet->SetAddressBook(watchonly_dest, "watchonly", wallet::AddressPurpose::RECEIVE);
240+
241+
// (b) Bech32 (P2WPKH): not a PKHash destination → not signable
242+
const WitnessV0KeyHash bech32_dest{GenerateRandomKey().GetPubKey()};
243+
wallet->SetAddressBook(bech32_dest, "bech32", wallet::AddressPurpose::RECEIVE);
244+
245+
// (c) Spendable PKHash: private key imported via pkh() descriptor → signable
246+
CKey legacy_key;
247+
legacy_key.MakeNewKey(true);
248+
FlatSigningProvider legacy_provider;
249+
std::string legacy_err;
250+
auto legacy_descs = Parse("pkh(" + EncodeSecret(legacy_key) + ")", legacy_provider, legacy_err, /*require_checksum=*/false);
251+
assert(!legacy_descs.empty());
252+
WalletDescriptor legacy_w_desc(std::move(legacy_descs[0]), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/1);
253+
QVERIFY(wallet->AddWalletDescriptor(legacy_w_desc, legacy_provider, "", /*internal=*/false));
254+
const PKHash legacy_dest{legacy_key.GetPubKey()};
255+
wallet->SetAddressBook(legacy_dest, "legacy", wallet::AddressPurpose::RECEIVE);
256+
}
257+
258+
std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
259+
OptionsModel optionsModel(node);
260+
bilingual_str error;
261+
QVERIFY(optionsModel.Init(error));
262+
ClientModel clientModel(node, &optionsModel);
263+
WalletContext& context = *node.walletLoader().context();
264+
AddWallet(context, wallet);
265+
WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get());
266+
RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
267+
AddressTableModel* addrModel = walletModel.getAddressTableModel();
268+
QVERIFY(addrModel);
269+
270+
// Only the spendable PKHash should report CanSignMessageRole=true
271+
int signable_count = 0;
272+
for (int i = 0; i < addrModel->rowCount({}); ++i) {
273+
if (addrModel->data(addrModel->index(i, AddressTableModel::Address, {}),
274+
AddressTableModel::CanSignMessageRole).toBool()) {
275+
++signable_count;
276+
}
277+
}
278+
QCOMPARE(signable_count, 1);
279+
280+
// AddressFilter::Signable must show only the spendable PKHash; no filter shows all
281+
AddressBookPage page_all{platformStyle.get(), AddressBookPage::ForSelection, AddressBookPage::ReceivingTab};
282+
page_all.setModel(addrModel);
283+
AddressBookPage page_signable{platformStyle.get(), AddressBookPage::ForSelection, AddressBookPage::ReceivingTab,
284+
nullptr, AddressBookPage::AddressFilter::Signable};
285+
page_signable.setModel(addrModel);
286+
287+
auto* table_all = page_all.findChild<QTableView*>("tableView");
288+
auto* table_signable = page_signable.findChild<QTableView*>("tableView");
289+
QVERIFY(table_all != nullptr);
290+
QVERIFY(table_signable != nullptr);
291+
QCOMPARE(table_signable->model()->rowCount(), 1);
292+
QVERIFY(table_all->model()->rowCount() >= 3);
293+
}
294+
213295
/**
214296
* Regression test for the stale shared model bug: clicking the address book
215297
* button in SignVerifyMessageDialog previously called
@@ -288,4 +370,5 @@ void AddressBookTests::addressBookTests()
288370
#endif
289371
TestAddAddressesToSendBook(m_node);
290372
TestAddressTableModelStability(m_node);
373+
TestSignableAddressFilter(m_node);
291374
}

0 commit comments

Comments
 (0)