Skip to content

Commit dd6f100

Browse files
committed
Issue #20: add sign/verify message backend adapter and tests
1 parent fc6dfca commit dd6f100

7 files changed

Lines changed: 394 additions & 0 deletions
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) 2026 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/models/signverifymessageadapter.h>
6+
7+
#include <bitcoin-build-config.h> // IWYU pragma: keep
8+
#include <key_io.h>
9+
10+
#include <QObject>
11+
12+
namespace {
13+
QString InvalidAddressMessage()
14+
{
15+
return QObject::tr("The entered address is invalid.") + QStringLiteral(" ")
16+
+ QObject::tr("Please check the address and try again.");
17+
}
18+
} // namespace
19+
20+
SignAddressValidationResult SignVerifyMessageAdapter::ValidateSigningAddress(const QString& address)
21+
{
22+
const QString trimmed_address = address.trimmed();
23+
if (trimmed_address.isEmpty()) {
24+
return {
25+
false,
26+
QObject::tr("Enter a Bitcoin address to sign with."),
27+
std::nullopt,
28+
};
29+
}
30+
31+
const CTxDestination destination = DecodeDestination(trimmed_address.toStdString());
32+
if (!IsValidDestination(destination)) {
33+
return {
34+
false,
35+
InvalidAddressMessage(),
36+
std::nullopt,
37+
};
38+
}
39+
40+
const PKHash* pkhash = std::get_if<PKHash>(&destination);
41+
if (!pkhash) {
42+
return {
43+
false,
44+
UnsupportedAddressTypeMessage(),
45+
std::nullopt,
46+
};
47+
}
48+
49+
return {
50+
true,
51+
QString{},
52+
*pkhash,
53+
};
54+
}
55+
56+
SignVerifyMessageResult SignVerifyMessageAdapter::BuildSigningResult(const SigningResult result,
57+
const QString& signature)
58+
{
59+
switch (result) {
60+
case SigningResult::OK:
61+
return {
62+
true,
63+
QObject::tr("Message signed."),
64+
signature,
65+
};
66+
case SigningResult::PRIVATE_KEY_NOT_AVAILABLE:
67+
return {
68+
false,
69+
QObject::tr("Private key for the entered address is not available."),
70+
QString{},
71+
};
72+
case SigningResult::SIGNING_FAILED:
73+
return {
74+
false,
75+
QObject::tr("Message signing failed."),
76+
QString{},
77+
};
78+
}
79+
80+
return {
81+
false,
82+
QObject::tr("Message signing failed."),
83+
QString{},
84+
};
85+
}
86+
87+
SignVerifyMessageResult SignVerifyMessageAdapter::VerifyMessage(const QString& address,
88+
const QString& signature,
89+
const QString& message)
90+
{
91+
const auto result = MessageVerify(address.toStdString(),
92+
signature.toStdString(),
93+
message.toStdString());
94+
95+
switch (result) {
96+
case MessageVerificationResult::OK:
97+
return {
98+
true,
99+
QObject::tr("Message verified."),
100+
QString{},
101+
};
102+
case MessageVerificationResult::ERR_INVALID_ADDRESS:
103+
return {
104+
false,
105+
InvalidAddressMessage(),
106+
QString{},
107+
};
108+
case MessageVerificationResult::ERR_ADDRESS_NO_KEY:
109+
return {
110+
false,
111+
UnsupportedAddressTypeMessage(),
112+
QString{},
113+
};
114+
case MessageVerificationResult::ERR_MALFORMED_SIGNATURE:
115+
return {
116+
false,
117+
QObject::tr("The signature could not be decoded.") + QStringLiteral(" ")
118+
+ QObject::tr("Please check the signature and try again."),
119+
QString{},
120+
};
121+
case MessageVerificationResult::ERR_PUBKEY_NOT_RECOVERED:
122+
return {
123+
false,
124+
QObject::tr("The signature did not match the message digest.") + QStringLiteral(" ")
125+
+ QObject::tr("Please check the signature and try again."),
126+
QString{},
127+
};
128+
case MessageVerificationResult::ERR_NOT_SIGNED:
129+
return {
130+
false,
131+
QObject::tr("Message verification failed."),
132+
QString{},
133+
};
134+
}
135+
136+
return {
137+
false,
138+
QObject::tr("Message verification failed."),
139+
QString{},
140+
};
141+
}
142+
143+
QString SignVerifyMessageAdapter::UnsupportedAddressTypeMessage()
144+
{
145+
return QObject::tr("The entered address does not refer to a legacy (P2PKH) key. "
146+
"Message signing for SegWit and other non-P2PKH address types is "
147+
"not supported in this version of %1. Please check the address and try again.")
148+
.arg(QStringLiteral(CLIENT_NAME));
149+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2026 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_QML_MODELS_SIGNVERIFYMESSAGEADAPTER_H
6+
#define BITCOIN_QML_MODELS_SIGNVERIFYMESSAGEADAPTER_H
7+
8+
#include <addresstype.h>
9+
#include <common/signmessage.h>
10+
11+
#include <QString>
12+
13+
#include <optional>
14+
15+
struct SignAddressValidationResult {
16+
bool success{false};
17+
QString message;
18+
std::optional<PKHash> keyhash;
19+
};
20+
21+
struct SignVerifyMessageResult {
22+
bool success{false};
23+
QString message;
24+
QString signature;
25+
};
26+
27+
class SignVerifyMessageAdapter
28+
{
29+
public:
30+
static SignAddressValidationResult ValidateSigningAddress(const QString& address);
31+
static SignVerifyMessageResult BuildSigningResult(SigningResult result, const QString& signature);
32+
static SignVerifyMessageResult VerifyMessage(const QString& address,
33+
const QString& signature,
34+
const QString& message);
35+
static QString UnsupportedAddressTypeMessage();
36+
};
37+
38+
#endif // BITCOIN_QML_MODELS_SIGNVERIFYMESSAGEADAPTER_H

qml/models/walletqmlmodel.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <qml/models/psbtoperationsadapter.h>
1212
#include <qml/models/sendrecipient.h>
1313
#include <qml/models/sendrecipientslistmodel.h>
14+
#include <qml/models/signverifymessageadapter.h>
1415
#include <qml/models/transactionrbfadapter.h>
1516
#include <qml/models/walletqmlmodeltransaction.h>
1617

@@ -150,6 +151,15 @@ QVariantMap BuildBip21ResultMap(const Bip21ParseResult& result)
150151
return payload;
151152
}
152153

154+
QVariantMap BuildSignVerifyResultMap(const SignVerifyMessageResult& result)
155+
{
156+
QVariantMap payload;
157+
payload[QStringLiteral("success")] = result.success;
158+
payload[QStringLiteral("message")] = result.message;
159+
payload[QStringLiteral("signature")] = result.signature;
160+
return payload;
161+
}
162+
153163
QString ResolveLocalDestinationPath(const QString& destination)
154164
{
155165
const QString trimmed = destination.trimmed();
@@ -941,6 +951,68 @@ QVariantMap WalletQmlModel::parseBitcoinUriFromFile(const QString& source_path)
941951
return BuildBip21ResultMap(Bip21Uri::Parse(file_text));
942952
}
943953

954+
QVariantMap WalletQmlModel::signMessage(const QString& address,
955+
const QString& message,
956+
const QString& passphrase)
957+
{
958+
if (!m_wallet) {
959+
return BuildSignVerifyResultMap({
960+
false,
961+
tr("No wallet is loaded."),
962+
QString{},
963+
});
964+
}
965+
966+
const SignAddressValidationResult validation = SignVerifyMessageAdapter::ValidateSigningAddress(address);
967+
if (!validation.success || !validation.keyhash.has_value()) {
968+
return BuildSignVerifyResultMap({
969+
false,
970+
validation.message,
971+
QString{},
972+
});
973+
}
974+
975+
bool relock_wallet = false;
976+
if (m_wallet->isCrypted() && m_wallet->isLocked()) {
977+
if (passphrase.trimmed().isEmpty()) {
978+
return BuildSignVerifyResultMap({
979+
false,
980+
tr("Enter your wallet passphrase to unlock and sign."),
981+
QString{},
982+
});
983+
}
984+
985+
const SecureString secure_passphrase{passphrase.toStdString()};
986+
if (!m_wallet->unlock(secure_passphrase)) {
987+
return BuildSignVerifyResultMap({
988+
false,
989+
tr("Wallet unlock failed."),
990+
QString{},
991+
});
992+
}
993+
relock_wallet = true;
994+
}
995+
996+
std::string signature;
997+
const SigningResult sign_result = m_wallet->signMessage(message.toStdString(), *validation.keyhash, signature);
998+
999+
if (relock_wallet) {
1000+
m_wallet->lock();
1001+
}
1002+
1003+
return BuildSignVerifyResultMap(SignVerifyMessageAdapter::BuildSigningResult(sign_result,
1004+
QString::fromStdString(signature)));
1005+
}
1006+
1007+
QVariantMap WalletQmlModel::verifyMessage(const QString& address,
1008+
const QString& message,
1009+
const QString& signature) const
1010+
{
1011+
return BuildSignVerifyResultMap(SignVerifyMessageAdapter::VerifyMessage(address,
1012+
signature,
1013+
message));
1014+
}
1015+
9441016
void WalletQmlModel::sendTransaction()
9451017
{
9461018
if (!m_wallet || !m_current_transaction) {

qml/models/walletqmlmodel.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ class WalletQmlModel : public QObject, public WalletBalanceProvider
7575
Q_INVOKABLE QVariantMap savePsbtToFile(const QString& psbt_base64, const QString& destination);
7676
Q_INVOKABLE QVariantMap parseBitcoinUri(const QString& uri_text) const;
7777
Q_INVOKABLE QVariantMap parseBitcoinUriFromFile(const QString& source_path) const;
78+
Q_INVOKABLE QVariantMap signMessage(const QString& address, const QString& message, const QString& passphrase = QString{});
79+
Q_INVOKABLE QVariantMap verifyMessage(const QString& address, const QString& message, const QString& signature) const;
7880
Q_INVOKABLE void sendTransaction();
7981
Q_INVOKABLE QString newAddress(QString label);
8082
bool isEncrypted() const;

test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ add_executable(bitcoinqml_unit_tests
3535
test_walletrestoreadapter.cpp
3636
test_walletmigrationadapter.cpp
3737
test_psbtoperationsadapter.cpp
38+
test_signverifymessageadapter.cpp
3839
test_initexecutor.cpp
3940
test_testbridge.cpp
4041
test_transaction.cpp
@@ -72,6 +73,7 @@ add_executable(bitcoinqml_unit_tests
7273
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/walletrestoreadapter.cpp
7374
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/walletmigrationadapter.cpp
7475
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/psbtoperationsadapter.cpp
76+
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/signverifymessageadapter.cpp
7577
${CMAKE_CURRENT_SOURCE_DIR}/../bitcoin/src/rpc/client.cpp
7678
${CMAKE_CURRENT_SOURCE_DIR}/../qml/test/testbridge.cpp
7779
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/transaction.cpp

0 commit comments

Comments
 (0)