Skip to content

Commit dcf2ccb

Browse files
committed
Merge #18115: wallet: Pass in transactions and messages for signing instead of exporting the private keys
d2774c0 Clear any input_errors for an input after it is signed (Andrew Chow) dc17488 Replace GetSigningProvider with GetSolvingProvider (Andrew Chow) 6a9c429 Move direct calls to MessageSign into new SignMessage functions in CWallet and ScriptPubKeyMan (Andrew Chow) 82a30fa Move key and script filling and signing from CWallet::FillPSBT to ScriptPubKeyMan::FillPSBT (Andrew Chow) 3d70dd9 Move FillPSBT to be a member of CWallet (Andrew Chow) a4af324 Use CWallet::SignTransaction in CreateTransaction and signrawtransactionwithwallet (Andrew Chow) f37de92 Implement CWallet::SignTransaction using ScriptPubKeyMan::SignTransaction (Andrew Chow) d999dd5 Add SignTransaction function to ScriptPubKeyMan and LegacyScriptPubKeyMan (Andrew Chow) 2c52b59 Refactor rawtransaction's SignTransaction into generic SignTransaction function (Andrew Chow) Pull request description: Following #17261, the way to sign transactions, PSBTs, and messages was to use `GetSigningProvider()` and get a `SigningProvider` containing the private keys. However this may not be feasible for future `ScriptPubKeyMan`s, such as for hardware wallets. Instead of exporting a `SigningProvider` containing private keys, we need to pass these things into the `ScriptPubKeyMan` (via `CWallet`) so that they can do whatever is needed internally to sign them. This is largely a refactor as the logic of processing transactions, PSBTs, and messages for is moved into `LegacyScriptPubKeyMan` and `CWallet` instead of being handled by the caller (e.g. `signrawtransaction`). To help with this, I've refactored the 3(!) implementations of a `SignTransaction()` function into one generic one. This function will be called by `signrawtransactionwithkey` and `LegacyScriptPubKeyMan::SignTransaction()`. `CWallet::CreateTransaction()` is changed to call `CWallet::SignTransaction()` which in turn, calls `LegacyScriptPubKeyMan::SignTransaction()`. Other `ScriptPubKeyMan`s may implement `SignTransaction()` differently. `FillPSBT()` is moved to be a member function of `CWallet` and the `psbtwallet.cpp/h` files removed. It is further split so that `CWallet` handles filling the UTXOs while the `ScriptPubKeyMan` handles adding keys, derivation paths, scripts, and signatures. In the end `LegacyScriptPubKeyMan::FillPSBT` still calls `SignPSBTInput`, but the `SigningProvider` is internal to `LegacyScriptPubKeyMan`. Other `ScriptPubKeyMan`s may do something different. A new `SignMessage()` function is added to both `CWallet` and `ScriptPubKeyMan`. Instead of having the caller (i.e. `signmessage` or the sign message dialog) get the private key, hash the message, and sign, `ScriptPubKeyMan` will now handle that (`CWallet` passes through to the `ScriptPubKeyMan`s as it does for many functions). This signing code is thus consolidated into `LegacyScriptPubKeyMan::SignMessage()`, though other `ScriptPubKeyMan`s may implement it differently. Additionally, a `SigningError` enum is introduced for the different errors that we expect to see from `SignMessage()`. Lastly, `GetSigningProvider()` is renamed to `GetPublicSigningProvider()`. It will now only provide pubkeys, key origins, and scripts. `LegacySigningProvider` has it's `GetKey` and `HaveKey` functions changed to only return false. Future implementations should return `HidingSigningProvider`s where private keys are hidden. Other things like `dumpprivkey` and `dumpwallet` are not changed because they directly need and access the `LegacyScriptPubKeyMan` so are not relevant to future changes. ACKs for top commit: instagibbs: reACK bitcoin/bitcoin@d2774c0 Sjors: re-utACK d2774c0 meshcollider: re-utACK d2774c0 Tree-SHA512: 89c83e7e7e9315e283fae145a2264648a9d7f7ace8f3281cb3f44f0b013c988d67ba4fa9726e50c643c0ed921bdd269adaec984840d11acf4a681f3e8a582cc1
2 parents ffdde47 + d2774c0 commit dcf2ccb

19 files changed

+427
-270
lines changed

src/Makefile.am

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ BITCOIN_CORE_H = \
241241
wallet/fees.h \
242242
wallet/ismine.h \
243243
wallet/load.h \
244-
wallet/psbtwallet.h \
245244
wallet/rpcwallet.h \
246245
wallet/scriptpubkeyman.h \
247246
wallet/wallet.h \
@@ -349,7 +348,6 @@ libbitcoin_wallet_a_SOURCES = \
349348
wallet/feebumper.cpp \
350349
wallet/fees.cpp \
351350
wallet/load.cpp \
352-
wallet/psbtwallet.cpp \
353351
wallet/rpcdump.cpp \
354352
wallet/rpcwallet.cpp \
355353
wallet/scriptpubkeyman.cpp \

src/interfaces/wallet.cpp

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
#include <wallet/fees.h>
2020
#include <wallet/ismine.h>
2121
#include <wallet/load.h>
22-
#include <wallet/psbtwallet.h>
2322
#include <wallet/rpcwallet.h>
2423
#include <wallet/wallet.h>
2524

@@ -119,19 +118,15 @@ class WalletImpl : public Wallet
119118
}
120119
bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) override
121120
{
122-
std::unique_ptr<SigningProvider> provider = m_wallet->GetSigningProvider(script);
121+
std::unique_ptr<SigningProvider> provider = m_wallet->GetSolvingProvider(script);
123122
if (provider) {
124123
return provider->GetPubKey(address, pub_key);
125124
}
126125
return false;
127126
}
128-
bool getPrivKey(const CScript& script, const CKeyID& address, CKey& key) override
127+
SigningResult signMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) override
129128
{
130-
std::unique_ptr<SigningProvider> provider = m_wallet->GetSigningProvider(script);
131-
if (provider) {
132-
return provider->GetKey(address, key);
133-
}
134-
return false;
129+
return m_wallet->SignMessage(message, pkhash, str_sig);
135130
}
136131
bool isSpendable(const CTxDestination& dest) override { return m_wallet->IsMine(dest) & ISMINE_SPENDABLE; }
137132
bool haveWatchOnly() override
@@ -361,9 +356,9 @@ class WalletImpl : public Wallet
361356
bool& complete,
362357
int sighash_type = 1 /* SIGHASH_ALL */,
363358
bool sign = true,
364-
bool bip32derivs = false) override
359+
bool bip32derivs = false) const override
365360
{
366-
return FillPSBT(m_wallet.get(), psbtx, complete, sighash_type, sign, bip32derivs);
361+
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs);
367362
}
368363
WalletBalances getBalances() override
369364
{

src/interfaces/wallet.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <script/standard.h> // For CTxDestination
1111
#include <support/allocators/secure.h> // For SecureString
1212
#include <ui_interface.h> // For ChangeType
13+
#include <util/message.h>
1314

1415
#include <functional>
1516
#include <map>
@@ -84,8 +85,8 @@ class Wallet
8485
//! Get public key.
8586
virtual bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) = 0;
8687

87-
//! Get private key.
88-
virtual bool getPrivKey(const CScript& script, const CKeyID& address, CKey& key) = 0;
88+
//! Sign message
89+
virtual SigningResult signMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) = 0;
8990

9091
//! Return whether wallet has private key.
9192
virtual bool isSpendable(const CTxDestination& dest) = 0;
@@ -196,7 +197,7 @@ class Wallet
196197
bool& complete,
197198
int sighash_type = 1 /* SIGHASH_ALL */,
198199
bool sign = true,
199-
bool bip32derivs = false) = 0;
200+
bool bip32derivs = false) const = 0;
200201

201202
//! Get balances.
202203
virtual WalletBalances getBalances() = 0;

src/qt/sendcoinsdialog.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
#include <ui_interface.h>
2727
#include <wallet/coincontrol.h>
2828
#include <wallet/fees.h>
29-
#include <wallet/psbtwallet.h>
3029
#include <wallet/wallet.h>
3130

3231
#include <QFontMetrics>

src/qt/signverifymessagedialog.cpp

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,27 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked()
133133
return;
134134
}
135135

136-
CKey key;
137-
if (!model->wallet().getPrivKey(GetScriptForDestination(destination), CKeyID(*pkhash), key))
138-
{
139-
ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }");
140-
ui->statusLabel_SM->setText(tr("Private key for the entered address is not available."));
141-
return;
142-
}
143-
144136
const std::string& message = ui->messageIn_SM->document()->toPlainText().toStdString();
145137
std::string signature;
138+
SigningResult res = model->wallet().signMessage(message, *pkhash, signature);
139+
140+
QString error;
141+
switch (res) {
142+
case SigningResult::OK:
143+
error = tr("No error");
144+
break;
145+
case SigningResult::PRIVATE_KEY_NOT_AVAILABLE:
146+
error = tr("Private key for the entered address is not available.");
147+
break;
148+
case SigningResult::SIGNING_FAILED:
149+
error = tr("Message signing failed.");
150+
break;
151+
// no default case, so the compiler can warn about missing cases
152+
}
146153

147-
if (!MessageSign(key, message, signature)) {
154+
if (res != SigningResult::OK) {
148155
ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }");
149-
ui->statusLabel_SM->setText(QString("<nobr>") + tr("Message signing failed.") + QString("</nobr>"));
156+
ui->statusLabel_SM->setText(QString("<nobr>") + error + QString("</nobr>"));
150157
return;
151158
}
152159

src/rpc/rawtransaction_util.cpp

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -272,55 +272,27 @@ void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore,
272272
{
273273
int nHashType = ParseSighashString(hashType);
274274

275-
bool fHashSingle = ((nHashType & ~SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE);
276-
277275
// Script verification errors
278-
UniValue vErrors(UniValue::VARR);
279-
280-
// Use CTransaction for the constant parts of the
281-
// transaction to avoid rehashing.
282-
const CTransaction txConst(mtx);
283-
// Sign what we can:
284-
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
285-
CTxIn& txin = mtx.vin[i];
286-
auto coin = coins.find(txin.prevout);
287-
if (coin == coins.end() || coin->second.IsSpent()) {
288-
TxInErrorToJSON(txin, vErrors, "Input not found or already spent");
289-
continue;
290-
}
291-
const CScript& prevPubKey = coin->second.out.scriptPubKey;
292-
const CAmount& amount = coin->second.out.nValue;
293-
294-
SignatureData sigdata = DataFromTransaction(mtx, i, coin->second.out);
295-
// Only sign SIGHASH_SINGLE if there's a corresponding output:
296-
if (!fHashSingle || (i < mtx.vout.size())) {
297-
ProduceSignature(*keystore, MutableTransactionSignatureCreator(&mtx, i, amount, nHashType), prevPubKey, sigdata);
298-
}
299-
300-
UpdateInput(txin, sigdata);
276+
std::map<int, std::string> input_errors;
301277

302-
// amount must be specified for valid segwit signature
303-
if (amount == MAX_MONEY && !txin.scriptWitness.IsNull()) {
304-
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Missing amount for %s", coin->second.out.ToString()));
305-
}
278+
bool complete = SignTransaction(mtx, keystore, coins, nHashType, input_errors);
279+
SignTransactionResultToJSON(mtx, complete, coins, input_errors, result);
280+
}
306281

307-
ScriptError serror = SCRIPT_ERR_OK;
308-
if (!VerifyScript(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount), &serror)) {
309-
if (serror == SCRIPT_ERR_INVALID_STACK_OPERATION) {
310-
// Unable to sign input and verification failed (possible attempt to partially sign).
311-
TxInErrorToJSON(txin, vErrors, "Unable to sign input, invalid stack size (possibly missing key)");
312-
} else if (serror == SCRIPT_ERR_SIG_NULLFAIL) {
313-
// Verification failed (possibly due to insufficient signatures).
314-
TxInErrorToJSON(txin, vErrors, "CHECK(MULTI)SIG failing with non-zero signature (possibly need more signatures)");
315-
} else {
316-
TxInErrorToJSON(txin, vErrors, ScriptErrorString(serror));
317-
}
282+
void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, std::map<int, std::string>& input_errors, UniValue& result)
283+
{
284+
// Make errors UniValue
285+
UniValue vErrors(UniValue::VARR);
286+
for (const auto& err_pair : input_errors) {
287+
if (err_pair.second == "Missing amount") {
288+
// This particular error needs to be an exception for some reason
289+
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Missing amount for %s", coins.at(mtx.vin.at(err_pair.first).prevout).out.ToString()));
318290
}
291+
TxInErrorToJSON(mtx.vin.at(err_pair.first), vErrors, err_pair.second);
319292
}
320-
bool fComplete = vErrors.empty();
321293

322294
result.pushKV("hex", EncodeHexTx(CTransaction(mtx)));
323-
result.pushKV("complete", fComplete);
295+
result.pushKV("complete", complete);
324296
if (!vErrors.empty()) {
325297
if (result.exists("errors")) {
326298
vErrors.push_backV(result["errors"].getValues());

src/rpc/rawtransaction_util.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define BITCOIN_RPC_RAWTRANSACTION_UTIL_H
77

88
#include <map>
9+
#include <string>
910

1011
class FillableSigningProvider;
1112
class UniValue;
@@ -24,6 +25,7 @@ class SigningProvider;
2425
* @param result JSON object where signed transaction results accumulate
2526
*/
2627
void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result);
28+
void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, std::map<int, std::string>& input_errors, UniValue& result);
2729

2830
/**
2931
* Parse a prevtxs UniValue array and get the map of coins from it

src/script/sign.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,54 @@ bool IsSegWitOutput(const SigningProvider& provider, const CScript& script)
465465
}
466466
return false;
467467
}
468+
469+
bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, int nHashType, std::map<int, std::string>& input_errors)
470+
{
471+
bool fHashSingle = ((nHashType & ~SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE);
472+
473+
// Use CTransaction for the constant parts of the
474+
// transaction to avoid rehashing.
475+
const CTransaction txConst(mtx);
476+
// Sign what we can:
477+
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
478+
CTxIn& txin = mtx.vin[i];
479+
auto coin = coins.find(txin.prevout);
480+
if (coin == coins.end() || coin->second.IsSpent()) {
481+
input_errors[i] = "Input not found or already spent";
482+
continue;
483+
}
484+
const CScript& prevPubKey = coin->second.out.scriptPubKey;
485+
const CAmount& amount = coin->second.out.nValue;
486+
487+
SignatureData sigdata = DataFromTransaction(mtx, i, coin->second.out);
488+
// Only sign SIGHASH_SINGLE if there's a corresponding output:
489+
if (!fHashSingle || (i < mtx.vout.size())) {
490+
ProduceSignature(*keystore, MutableTransactionSignatureCreator(&mtx, i, amount, nHashType), prevPubKey, sigdata);
491+
}
492+
493+
UpdateInput(txin, sigdata);
494+
495+
// amount must be specified for valid segwit signature
496+
if (amount == MAX_MONEY && !txin.scriptWitness.IsNull()) {
497+
input_errors[i] = "Missing amount";
498+
continue;
499+
}
500+
501+
ScriptError serror = SCRIPT_ERR_OK;
502+
if (!VerifyScript(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount), &serror)) {
503+
if (serror == SCRIPT_ERR_INVALID_STACK_OPERATION) {
504+
// Unable to sign input and verification failed (possible attempt to partially sign).
505+
input_errors[i] = "Unable to sign input, invalid stack size (possibly missing key)";
506+
} else if (serror == SCRIPT_ERR_SIG_NULLFAIL) {
507+
// Verification failed (possibly due to insufficient signatures).
508+
input_errors[i] = "CHECK(MULTI)SIG failing with non-zero signature (possibly need more signatures)";
509+
} else {
510+
input_errors[i] = ScriptErrorString(serror);
511+
}
512+
} else {
513+
// If this input succeeds, make sure there is no error set for it
514+
input_errors.erase(i);
515+
}
516+
}
517+
return input_errors.empty();
518+
}

src/script/sign.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#ifndef BITCOIN_SCRIPT_SIGN_H
77
#define BITCOIN_SCRIPT_SIGN_H
88

9+
#include <coins.h>
910
#include <hash.h>
1011
#include <pubkey.h>
1112
#include <script/interpreter.h>
@@ -168,4 +169,7 @@ bool IsSolvable(const SigningProvider& provider, const CScript& script);
168169
/** Check whether a scriptPubKey is known to be segwit. */
169170
bool IsSegWitOutput(const SigningProvider& provider, const CScript& script);
170171

172+
/** Sign the CMutableTransaction */
173+
bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* provider, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors);
174+
171175
#endif // BITCOIN_SCRIPT_SIGN_H

src/util/message.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,17 @@ uint256 MessageHash(const std::string& message)
7676

7777
return hasher.GetHash();
7878
}
79+
80+
std::string SigningResultString(const SigningResult res)
81+
{
82+
switch (res) {
83+
case SigningResult::OK:
84+
return "No error";
85+
case SigningResult::PRIVATE_KEY_NOT_AVAILABLE:
86+
return "Private key not available";
87+
case SigningResult::SIGNING_FAILED:
88+
return "Sign failed";
89+
// no default case, so the compiler can warn about missing cases
90+
}
91+
assert(false);
92+
}

0 commit comments

Comments
 (0)