From cfc3dd34284357262bcc7eef2714a210891276c0 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 10 Jun 2015 00:03:08 -0700 Subject: [PATCH 01/10] Also remove pay-2-pubkey from watch when adding a priv key --- src/script/standard.cpp | 5 +++++ src/script/standard.h | 1 + src/wallet/wallet.cpp | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/script/standard.cpp b/src/script/standard.cpp index 66657127a..1d5aac7b3 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -286,6 +286,11 @@ CScript GetScriptForDestination(const CTxDestination& dest) return script; } +CScript GetScriptForRawPubKey(const CPubKey& pubKey) +{ + return CScript() << std::vector(pubKey.begin(), pubKey.end()) << OP_CHECKSIG; +} + CScript GetScriptForMultisig(int nRequired, const std::vector& keys) { CScript script; diff --git a/src/script/standard.h b/src/script/standard.h index 46ae5f9f1..9e17dac70 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -73,6 +73,7 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet) bool ExtractDestinations(const CScript& scriptPubKey, txnouttype& typeRet, std::vector& addressRet, int& nRequiredRet); CScript GetScriptForDestination(const CTxDestination& dest); +CScript GetScriptForRawPubKey(const CPubKey& pubkey); CScript GetScriptForMultisig(int nRequired, const std::vector& keys); #endif // BITCOIN_SCRIPT_STANDARD_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 7b3cd9803..c11800863 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -106,6 +106,9 @@ bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey) // check if we need to remove from watch-only CScript script; script = GetScriptForDestination(pubkey.GetID()); + if (HaveWatchOnly(script)) + RemoveWatchOnly(script); + script = GetScriptForRawPubKey(pubkey); if (HaveWatchOnly(script)) RemoveWatchOnly(script); From 983d2d90af1b517bee51170d2ea059e68d09be35 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 11 Jun 2015 00:57:26 -0700 Subject: [PATCH 02/10] Split up importaddress into helper functions --- src/wallet/rpcdump.cpp | 67 +++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 5f800474a..f56ff65f8 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -146,6 +146,26 @@ UniValue importprivkey(const UniValue& params, bool fHelp) return NullUniValue; } +void ImportScript(const CScript& script) +{ + if (::IsMine(*pwalletMain, script) == ISMINE_SPENDABLE) + throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script"); + + pwalletMain->MarkDirty(); + + if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script)) + throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); +} + +void ImportAddress(const CBitcoinAddress& address, const string& strLabel) +{ + CScript script = GetScriptForDestination(address.Get()); + ImportScript(script, false); + // add to address book or update label + if (address.IsValid()) + pwalletMain->SetAddressBook(address.Get(), strLabel, "receive"); +} + UniValue importaddress(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) @@ -172,20 +192,6 @@ UniValue importaddress(const UniValue& params, bool fHelp) if (fPruneMode) throw JSONRPCError(RPC_WALLET_ERROR, "Importing addresses is disabled in pruned mode"); - LOCK2(cs_main, pwalletMain->cs_wallet); - - CScript script; - - CBitcoinAddress address(params[0].get_str()); - if (address.IsValid()) { - script = GetScriptForDestination(address.Get()); - } else if (IsHex(params[0].get_str())) { - std::vector data(ParseHex(params[0].get_str())); - script = CScript(data.begin(), data.end()); - } else { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script"); - } - string strLabel = ""; if (params.size() > 1) strLabel = params[1].get_str(); @@ -195,28 +201,23 @@ UniValue importaddress(const UniValue& params, bool fHelp) if (params.size() > 2) fRescan = params[2].get_bool(); - { - if (::IsMine(*pwalletMain, script) == ISMINE_SPENDABLE) - throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script"); - // add to address book or update label - if (address.IsValid()) - pwalletMain->SetAddressBook(address.Get(), strLabel, "receive"); - - // Don't throw error in case an address is already there - if (pwalletMain->HaveWatchOnly(script)) - return NullUniValue; - - pwalletMain->MarkDirty(); + LOCK2(cs_main, pwalletMain->cs_wallet); - if (!pwalletMain->AddWatchOnly(script)) - throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); + CBitcoinAddress address(params[0].get_str()); + if (address.IsValid()) { + ImportAddress(address, strLabel); + } else if (IsHex(params[0].get_str())) { + std::vector data(ParseHex(params[0].get_str())); + ImportScript(CScript(data.begin(), data.end())); + } else { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script"); + } - if (fRescan) - { - pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); - pwalletMain->ReacceptWalletTransactions(); - } + if (fRescan) + { + pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); + pwalletMain->ReacceptWalletTransactions(); } return NullUniValue; From 907a425aa5b8fd90cf1d28215712a309e934b364 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 11 Jun 2015 00:57:50 -0700 Subject: [PATCH 03/10] Add p2sh option to importaddress to import redeemScripts --- qa/rpc-tests/listtransactions.py | 10 ++++++++++ src/rpcclient.cpp | 1 + src/wallet/rpcdump.cpp | 26 ++++++++++++++++++++------ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/qa/rpc-tests/listtransactions.py b/qa/rpc-tests/listtransactions.py index eeae2d2fa..b30a6bc9d 100755 --- a/qa/rpc-tests/listtransactions.py +++ b/qa/rpc-tests/listtransactions.py @@ -93,6 +93,16 @@ class ListTransactionsTest(BitcoinTestFramework): {"category":"receive","amount":Decimal("0.44")}, {"txid":txid, "account" : "toself"} ) + multisig = self.nodes[1].createmultisig(1, [self.nodes[1].getnewaddress()]) + self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) + txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) + self.nodes[1].generate(1) + self.sync_all() + assert(len(self.nodes[0].listtransactions("watchonly", 100, 0, False)) == 0) + check_array_result(self.nodes[0].listtransactions("watchonly", 100, 0, True), + {"category":"receive","amount":Decimal("0.1")}, + {"txid":txid, "account" : "watchonly"} ) + if __name__ == '__main__': ListTransactionsTest().main() diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index b41e960e8..450f33b3e 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -87,6 +87,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "lockunspent", 1 }, { "importprivkey", 2 }, { "importaddress", 2 }, + { "importaddress", 3 }, { "verifychain", 0 }, { "verifychain", 1 }, { "keypoolrefill", 0 }, diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index f56ff65f8..3493efc8f 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -146,21 +146,28 @@ UniValue importprivkey(const UniValue& params, bool fHelp) return NullUniValue; } -void ImportScript(const CScript& script) +void ImportAddress(const CBitcoinAddress& address, const string& strLabel); +void ImportScript(const CScript& script, const string& strLabel, bool isRedeemScript) { - if (::IsMine(*pwalletMain, script) == ISMINE_SPENDABLE) + if (!isRedeemScript && ::IsMine(*pwalletMain, script) == ISMINE_SPENDABLE) throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script"); pwalletMain->MarkDirty(); if (!pwalletMain->HaveWatchOnly(script) && !pwalletMain->AddWatchOnly(script)) throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); + + if (isRedeemScript) { + if (!pwalletMain->HaveCScript(script) && !pwalletMain->AddCScript(script)) + throw JSONRPCError(RPC_WALLET_ERROR, "Error adding p2sh redeemScript to wallet"); + ImportAddress(CBitcoinAddress(CScriptID(script)), strLabel); + } } void ImportAddress(const CBitcoinAddress& address, const string& strLabel) { CScript script = GetScriptForDestination(address.Get()); - ImportScript(script, false); + ImportScript(script, strLabel, false); // add to address book or update label if (address.IsValid()) pwalletMain->SetAddressBook(address.Get(), strLabel, "receive"); @@ -171,14 +178,15 @@ UniValue importaddress(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 3) + if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( - "importaddress \"address\" ( \"label\" rescan )\n" + "importaddress \"address\" ( \"label\" rescan p2sh )\n" "\nAdds an address or script (in hex) that can be watched as if it were in your wallet but cannot be used to spend.\n" "\nArguments:\n" "1. \"address\" (string, required) The address\n" "2. \"label\" (string, optional, default=\"\") An optional label\n" "3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n" + "4. p2sh (boolean, optional, default=false) Add the P2SH version of the script as well\n" "\nNote: This call can take minutes to complete if rescan is true.\n" "\nExamples:\n" "\nImport an address with rescan\n" @@ -201,15 +209,21 @@ UniValue importaddress(const UniValue& params, bool fHelp) if (params.size() > 2) fRescan = params[2].get_bool(); + // Whether to import a p2sh version, too + bool fP2SH = false; + if (params.size() > 3) + fP2SH = params[3].get_bool(); LOCK2(cs_main, pwalletMain->cs_wallet); CBitcoinAddress address(params[0].get_str()); if (address.IsValid()) { + if (fP2SH) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot use the p2sh flag with an address - use a script instead"); ImportAddress(address, strLabel); } else if (IsHex(params[0].get_str())) { std::vector data(ParseHex(params[0].get_str())); - ImportScript(CScript(data.begin(), data.end())); + ImportScript(CScript(data.begin(), data.end()), strLabel, fP2SH); } else { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script"); } From a1d7df32360605724d8f0ea4b7aebfa7aea24c97 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 9 Jul 2015 22:47:36 -0700 Subject: [PATCH 04/10] Add importpubkey method to import a watch-only pubkey --- src/rpcclient.cpp | 1 + src/rpcserver.cpp | 1 + src/rpcserver.h | 1 + src/wallet/rpcdump.cpp | 57 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 450f33b3e..0c8e6d6d6 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -88,6 +88,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importprivkey", 2 }, { "importaddress", 2 }, { "importaddress", 3 }, + { "importpubkey", 2 }, { "verifychain", 0 }, { "verifychain", 1 }, { "keypoolrefill", 0 }, diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index bcad06a0c..158603b14 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -359,6 +359,7 @@ static const CRPCCommand vRPCCommands[] = { "wallet", "importprivkey", &importprivkey, true }, { "wallet", "importwallet", &importwallet, true }, { "wallet", "importaddress", &importaddress, true }, + { "wallet", "importpubkey", &importpubkey, true }, { "wallet", "keypoolrefill", &keypoolrefill, true }, { "wallet", "listaccounts", &listaccounts, false }, { "wallet", "listaddressgroupings", &listaddressgroupings, false }, diff --git a/src/rpcserver.h b/src/rpcserver.h index 89d398022..3a71fd510 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -161,6 +161,7 @@ extern UniValue clearbanned(const UniValue& params, bool fHelp); extern UniValue dumpprivkey(const UniValue& params, bool fHelp); // in rpcdump.cpp extern UniValue importprivkey(const UniValue& params, bool fHelp); extern UniValue importaddress(const UniValue& params, bool fHelp); +extern UniValue importpubkey(const UniValue& params, bool fHelp); extern UniValue dumpwallet(const UniValue& params, bool fHelp); extern UniValue importwallet(const UniValue& params, bool fHelp); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 3493efc8f..7efabbfeb 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -237,6 +237,63 @@ UniValue importaddress(const UniValue& params, bool fHelp) return NullUniValue; } +UniValue importpubkey(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() < 1 || params.size() > 4) + throw runtime_error( + "importpubkey \"pubkey\" ( \"label\" rescan )\n" + "\nAdds a public key (in hex) that can be watched as if it were in your wallet but cannot be used to spend.\n" + "\nArguments:\n" + "1. \"pubkey\" (string, required) The hex-encoded public key\n" + "2. \"label\" (string, optional, default=\"\") An optional label\n" + "3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n" + "\nNote: This call can take minutes to complete if rescan is true.\n" + "\nExamples:\n" + "\nImport a public key with rescan\n" + + HelpExampleCli("importpubkey", "\"mypubkey\"") + + "\nImport using a label without rescan\n" + + HelpExampleCli("importpubkey", "\"mypubkey\" \"testing\" false") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("importpubkey", "\"mypubkey\", \"testing\", false") + ); + + if (fPruneMode) + throw JSONRPCError(RPC_WALLET_ERROR, "Importing public keys is disabled in pruned mode"); + + string strLabel = ""; + if (params.size() > 1) + strLabel = params[1].get_str(); + + // Whether to perform rescan after import + bool fRescan = true; + if (params.size() > 2) + fRescan = params[2].get_bool(); + + if (!IsHex(params[0].get_str())) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey must be a hex string"); + std::vector data(ParseHex(params[0].get_str())); + CPubKey pubKey(data.begin(), data.end()); + if (!pubKey.IsFullyValid()) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey is not a valid public key"); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + ImportAddress(CBitcoinAddress(pubKey.GetID()), strLabel); + ImportScript(GetScriptForRawPubKey(pubKey), strLabel, false); + + if (fRescan) + { + pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); + pwalletMain->ReacceptWalletTransactions(); + } + + return NullUniValue; +} + + UniValue importwallet(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) From 5c17059872c9b63a1e05c7aa8aea32a03c3ec73a Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 9 Jul 2015 22:47:47 -0700 Subject: [PATCH 05/10] Update importaddress help to push its use to script-only --- src/wallet/rpcdump.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 7efabbfeb..2c4f1f243 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -181,20 +181,21 @@ UniValue importaddress(const UniValue& params, bool fHelp) if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( "importaddress \"address\" ( \"label\" rescan p2sh )\n" - "\nAdds an address or script (in hex) that can be watched as if it were in your wallet but cannot be used to spend.\n" + "\nAdds a script (in hex) or address that can be watched as if it were in your wallet but cannot be used to spend.\n" "\nArguments:\n" - "1. \"address\" (string, required) The address\n" + "1. \"script\" (string, required) The hex-encoded script (or address)\n" "2. \"label\" (string, optional, default=\"\") An optional label\n" "3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n" "4. p2sh (boolean, optional, default=false) Add the P2SH version of the script as well\n" "\nNote: This call can take minutes to complete if rescan is true.\n" + "If you have the full public key, you should call importpublickey instead of this.\n" "\nExamples:\n" - "\nImport an address with rescan\n" - + HelpExampleCli("importaddress", "\"myaddress\"") + + "\nImport a script with rescan\n" + + HelpExampleCli("importaddress", "\"myscript\"") + "\nImport using a label without rescan\n" - + HelpExampleCli("importaddress", "\"myaddress\" \"testing\" false") + + + HelpExampleCli("importaddress", "\"myscript\" \"testing\" false") + "\nAs a JSON-RPC call\n" - + HelpExampleRpc("importaddress", "\"myaddress\", \"testing\", false") + + HelpExampleRpc("importaddress", "\"myscript\", \"testing\", false") ); if (fPruneMode) From d3354c52d7c0c6446cad4074c1d0e04bb1b3d84e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 9 Jun 2015 23:36:36 -0700 Subject: [PATCH 06/10] Add have-pubkey distinction to ISMINE flags This indicates that, eg, we have a public key for a key which may be used as a pay-to-pubkey-hash. It generally means that we can create a valid scriptSig except for missing private key(s) with which to create signatures. --- src/qt/transactiondesc.cpp | 4 ++-- src/qt/transactionrecord.cpp | 6 +++--- src/wallet/wallet_ismine.cpp | 10 +++++++--- src/wallet/wallet_ismine.h | 8 ++++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index af78a51d0..801c6c62d 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -165,7 +165,7 @@ QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx, TransactionReco if (fAllFromMe) { - if(fAllFromMe == ISMINE_WATCH_ONLY) + if(fAllFromMe & ISMINE_WATCH_ONLY) strHTML += "" + tr("From") + ": " + tr("watch-only") + "
"; // @@ -190,7 +190,7 @@ QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx, TransactionReco strHTML += GUIUtil::HtmlEscape(CBitcoinAddress(address).ToString()); if(toSelf == ISMINE_SPENDABLE) strHTML += " (own address)"; - else if(toSelf == ISMINE_WATCH_ONLY) + else if(toSelf & ISMINE_WATCH_ONLY) strHTML += " (watch-only)"; strHTML += "
"; } diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 15d13e9fc..d8623daf5 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -56,7 +56,7 @@ QList TransactionRecord::decomposeTransaction(const CWallet * CTxDestination address; sub.idx = parts.size(); // sequence number sub.credit = txout.nValue; - sub.involvesWatchAddress = mine == ISMINE_WATCH_ONLY; + sub.involvesWatchAddress = mine & ISMINE_WATCH_ONLY; if (ExtractDestination(txout.scriptPubKey, address) && IsMine(*wallet, address)) { // Received by Bitcoin Address @@ -86,7 +86,7 @@ QList TransactionRecord::decomposeTransaction(const CWallet * BOOST_FOREACH(const CTxIn& txin, wtx.vin) { isminetype mine = wallet->IsMine(txin); - if(mine == ISMINE_WATCH_ONLY) involvesWatchAddress = true; + if(mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; if(fAllFromMe > mine) fAllFromMe = mine; } @@ -94,7 +94,7 @@ QList TransactionRecord::decomposeTransaction(const CWallet * BOOST_FOREACH(const CTxOut& txout, wtx.vout) { isminetype mine = wallet->IsMine(txout); - if(mine == ISMINE_WATCH_ONLY) involvesWatchAddress = true; + if(mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; if(fAllToMe > mine) fAllToMe = mine; } diff --git a/src/wallet/wallet_ismine.cpp b/src/wallet/wallet_ismine.cpp index 5482348e3..0303cbb2f 100644 --- a/src/wallet/wallet_ismine.cpp +++ b/src/wallet/wallet_ismine.cpp @@ -9,6 +9,7 @@ #include "keystore.h" #include "script/script.h" #include "script/standard.h" +#include "script/sign.h" #include @@ -40,7 +41,7 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey) txnouttype whichType; if (!Solver(scriptPubKey, whichType, vSolutions)) { if (keystore.HaveWatchOnly(scriptPubKey)) - return ISMINE_WATCH_ONLY; + return ISMINE_WATCH_NOPUBKEY; return ISMINE_NO; } @@ -85,7 +86,10 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey) } } - if (keystore.HaveWatchOnly(scriptPubKey)) - return ISMINE_WATCH_ONLY; + if (keystore.HaveWatchOnly(scriptPubKey)) { + // TODO: This could be optimized some by doing some work after the above solver + CScript scriptSig; + return ProduceSignature(DummySignatureCreator(&keystore), scriptPubKey, scriptSig) ? ISMINE_WATCH_PUBKEY : ISMINE_WATCH_NOPUBKEY; + } return ISMINE_NO; } diff --git a/src/wallet/wallet_ismine.h b/src/wallet/wallet_ismine.h index 5b9b0e084..12afad175 100644 --- a/src/wallet/wallet_ismine.h +++ b/src/wallet/wallet_ismine.h @@ -16,8 +16,12 @@ class CScript; enum isminetype { ISMINE_NO = 0, - ISMINE_WATCH_ONLY = 1, - ISMINE_SPENDABLE = 2, + //! Indicates that we dont know how to create a scriptSig that would solve this if we were given the appropriate private keys + ISMINE_WATCH_NOPUBKEY = 1, + //! Indicates that we know how to create a scriptSig that would solve this if we were given the appropriate private keys + ISMINE_WATCH_PUBKEY = 2, + ISMINE_WATCH_ONLY = ISMINE_WATCH_NOPUBKEY | ISMINE_WATCH_PUBKEY, + ISMINE_SPENDABLE = 4, ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE }; /** used for bitflags of isminetype */ From f5813bdd3eb93a2a8d7ba01989eef5b299fcbca4 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 10 Jun 2015 01:04:08 -0700 Subject: [PATCH 07/10] Add logic to track pubkeys as watch-only, not just scripts --- src/keystore.cpp | 41 ++++++++++++++++++++++++++++++++------ src/keystore.h | 5 ++++- src/qt/sendcoinsdialog.cpp | 3 +-- src/qt/walletmodel.cpp | 5 +++++ src/qt/walletmodel.h | 1 + src/wallet/crypter.cpp | 4 +++- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/keystore.cpp b/src/keystore.cpp index 3bae24b7b..cf49ba83a 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -6,23 +6,30 @@ #include "keystore.h" #include "key.h" +#include "pubkey.h" #include "util.h" #include -bool CKeyStore::GetPubKey(const CKeyID &address, CPubKey &vchPubKeyOut) const +bool CKeyStore::AddKey(const CKey &key) { + return AddKeyPubKey(key, key.GetPubKey()); +} + +bool CBasicKeyStore::GetPubKey(const CKeyID &address, CPubKey &vchPubKeyOut) const { CKey key; - if (!GetKey(address, key)) + if (!GetKey(address, key)) { + WatchKeyMap::const_iterator it = mapWatchKeys.find(address); + if (it != mapWatchKeys.end()) { + vchPubKeyOut = it->second; + return true; + } return false; + } vchPubKeyOut = key.GetPubKey(); return true; } -bool CKeyStore::AddKey(const CKey &key) { - return AddKeyPubKey(key, key.GetPubKey()); -} - bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey) { LOCK(cs_KeyStore); @@ -58,10 +65,29 @@ bool CBasicKeyStore::GetCScript(const CScriptID &hash, CScript& redeemScriptOut) return false; } +static bool ExtractPubKey(const CScript &dest, CPubKey& pubKeyOut) +{ + //TODO: Use Solver to extract this? + CScript::const_iterator pc = dest.begin(); + opcodetype opcode; + std::vector vch; + if (!dest.GetOp(pc, opcode, vch) || vch.size() < 33 || vch.size() > 65) + return false; + pubKeyOut = CPubKey(vch); + if (!pubKeyOut.IsFullyValid()) + return false; + if (!dest.GetOp(pc, opcode, vch) || opcode != OP_CHECKSIG || dest.GetOp(pc, opcode, vch)) + return false; + return true; +} + bool CBasicKeyStore::AddWatchOnly(const CScript &dest) { LOCK(cs_KeyStore); setWatchOnly.insert(dest); + CPubKey pubKey; + if (ExtractPubKey(dest, pubKey)) + mapWatchKeys[pubKey.GetID()] = pubKey; return true; } @@ -69,6 +95,9 @@ bool CBasicKeyStore::RemoveWatchOnly(const CScript &dest) { LOCK(cs_KeyStore); setWatchOnly.erase(dest); + CPubKey pubKey; + if (ExtractPubKey(dest, pubKey)) + mapWatchKeys.erase(pubKey.GetID()); return true; } diff --git a/src/keystore.h b/src/keystore.h index 4a4b6d20a..b917bf20b 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -32,7 +32,7 @@ public: virtual bool HaveKey(const CKeyID &address) const =0; virtual bool GetKey(const CKeyID &address, CKey& keyOut) const =0; virtual void GetKeys(std::set &setAddress) const =0; - virtual bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const; + virtual bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const =0; //! Support for BIP 0013 : see https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki virtual bool AddCScript(const CScript& redeemScript) =0; @@ -47,6 +47,7 @@ public: }; typedef std::map KeyMap; +typedef std::map WatchKeyMap; typedef std::map ScriptMap; typedef std::set WatchOnlySet; @@ -55,11 +56,13 @@ class CBasicKeyStore : public CKeyStore { protected: KeyMap mapKeys; + WatchKeyMap mapWatchKeys; ScriptMap mapScripts; WatchOnlySet setWatchOnly; public: bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey); + bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const; bool HaveKey(const CKeyID &address) const { bool result; diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index e13cd714a..34da38285 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -752,10 +752,9 @@ void SendCoinsDialog::coinControlChangeEdited(const QString& text) } else // Valid address { - CPubKey pubkey; CKeyID keyid; addr.GetKeyID(keyid); - if (!model->getPubKey(keyid, pubkey)) // Unknown change address + if (!model->havePrivKey(keyid)) // Unknown change address { ui->labelCoinControlChangeLabel->setText(tr("Warning: Unknown change address")); } diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 168a0255f..4e3d97fc4 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -554,6 +554,11 @@ bool WalletModel::getPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const return wallet->GetPubKey(address, vchPubKeyOut); } +bool WalletModel::havePrivKey(const CKeyID &address) const +{ + return wallet->HaveKey(address); +} + // returns a list of COutputs from COutPoints void WalletModel::getOutputs(const std::vector& vOutpoints, std::vector& vOutputs) { diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 40bc62354..1c1684b27 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -186,6 +186,7 @@ public: UnlockContext requestUnlock(); bool getPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const; + bool havePrivKey(const CKeyID &address) const; void getOutputs(const std::vector& vOutpoints, std::vector& vOutputs); bool isSpent(const COutPoint& outpoint) const; void listCoins(std::map >& mapCoins) const; diff --git a/src/wallet/crypter.cpp b/src/wallet/crypter.cpp index c7f7e2167..a3e28f6ac 100644 --- a/src/wallet/crypter.cpp +++ b/src/wallet/crypter.cpp @@ -255,7 +255,7 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co { LOCK(cs_KeyStore); if (!IsCrypted()) - return CKeyStore::GetPubKey(address, vchPubKeyOut); + return CBasicKeyStore::GetPubKey(address, vchPubKeyOut); CryptedKeyMap::const_iterator mi = mapCryptedKeys.find(address); if (mi != mapCryptedKeys.end()) @@ -263,6 +263,8 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co vchPubKeyOut = (*mi).second.first; return true; } + // Check for watch-only pubkeys + return CBasicKeyStore::GetPubKey(address, vchPubKeyOut); } return false; } From 6bdb474dc9dd34e1a5b13ce9494a936cba77e027 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 23 Apr 2015 21:42:49 -0700 Subject: [PATCH 08/10] Implement watchonly support in fundrawtransaction Some code and test cases stolen from Bryan Bishop (pull #5524). --- qa/rpc-tests/fundrawtransaction.py | 59 ++++++++++++++++++++++++++++-- src/coincontrol.h | 3 ++ src/wallet/rpcwallet.cpp | 19 +++++++--- src/wallet/wallet.cpp | 7 +++- src/wallet/wallet.h | 2 +- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/qa/rpc-tests/fundrawtransaction.py b/qa/rpc-tests/fundrawtransaction.py index 80f1d1e12..deaf8b68f 100755 --- a/qa/rpc-tests/fundrawtransaction.py +++ b/qa/rpc-tests/fundrawtransaction.py @@ -13,14 +13,15 @@ class RawTransactionsTest(BitcoinTestFramework): def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) - initialize_chain_clean(self.options.tmpdir, 3) + initialize_chain_clean(self.options.tmpdir, 4) def setup_network(self, split=False): - self.nodes = start_nodes(3, self.options.tmpdir) + self.nodes = start_nodes(4, self.options.tmpdir) connect_nodes_bi(self.nodes,0,1) connect_nodes_bi(self.nodes,1,2) connect_nodes_bi(self.nodes,0,2) + connect_nodes_bi(self.nodes,0,3) self.is_network_split=False self.sync_all() @@ -31,11 +32,20 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[2].generate(1) self.sync_all() - self.nodes[0].generate(101) + self.nodes[0].generate(121) self.sync_all() + + watchonly_address = self.nodes[0].getnewaddress() + watchonly_pubkey = self.nodes[0].validateaddress(watchonly_address)["pubkey"] + watchonly_amount = 200 + self.nodes[3].importpubkey(watchonly_pubkey, "", True) + watchonly_txid = self.nodes[0].sendtoaddress(watchonly_address, watchonly_amount) + self.nodes[0].sendtoaddress(self.nodes[3].getnewaddress(), watchonly_amount / 10); + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.5); self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.0); self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),5.0); + self.sync_all() self.nodes[0].generate(1) self.sync_all() @@ -428,11 +438,12 @@ class RawTransactionsTest(BitcoinTestFramework): stop_nodes(self.nodes) wait_bitcoinds() - self.nodes = start_nodes(3, self.options.tmpdir) + self.nodes = start_nodes(4, self.options.tmpdir) connect_nodes_bi(self.nodes,0,1) connect_nodes_bi(self.nodes,1,2) connect_nodes_bi(self.nodes,0,2) + connect_nodes_bi(self.nodes,0,3) self.is_network_split=False self.sync_all() @@ -525,5 +536,45 @@ class RawTransactionsTest(BitcoinTestFramework): assert_equal(oldBalance+Decimal('50.19000000'), self.nodes[0].getbalance()) #0.19+block reward + ################################################## + # test a fundrawtransaction using only watchonly # + ################################################## + + inputs = [] + outputs = {self.nodes[2].getnewaddress() : watchonly_amount / 2} + rawtx = self.nodes[3].createrawtransaction(inputs, outputs) + + result = self.nodes[3].fundrawtransaction(rawtx, True) + res_dec = self.nodes[0].decoderawtransaction(result["hex"]) + assert_equal(len(res_dec["vin"]), 1) + assert_equal(res_dec["vin"][0]["txid"], watchonly_txid) + + assert_equal("fee" in result.keys(), True) + assert_greater_than(result["changepos"], -1) + + ############################################################### + # test fundrawtransaction using the entirety of watched funds # + ############################################################### + + inputs = [] + outputs = {self.nodes[2].getnewaddress() : watchonly_amount} + rawtx = self.nodes[3].createrawtransaction(inputs, outputs) + + result = self.nodes[3].fundrawtransaction(rawtx, True) + res_dec = self.nodes[0].decoderawtransaction(result["hex"]) + assert_equal(len(res_dec["vin"]), 2) + assert(res_dec["vin"][0]["txid"] == watchonly_txid or res_dec["vin"][1]["txid"] == watchonly_txid) + + assert_greater_than(result["fee"], 0) + assert_greater_than(result["changepos"], -1) + assert_equal(result["fee"] + res_dec["vout"][result["changepos"]]["value"], watchonly_amount / 10) + + signedtx = self.nodes[3].signrawtransaction(result["hex"]) + assert(not signedtx["complete"]) + signedtx = self.nodes[0].signrawtransaction(signedtx["hex"]) + assert(signedtx["complete"]) + self.nodes[0].sendrawtransaction(signedtx["hex"]) + + if __name__ == '__main__': RawTransactionsTest().main() diff --git a/src/coincontrol.h b/src/coincontrol.h index 3e8de83c3..74d524c0a 100644 --- a/src/coincontrol.h +++ b/src/coincontrol.h @@ -14,6 +14,8 @@ public: CTxDestination destChange; //! If false, allows unselected inputs, but requires all selected inputs be used bool fAllowOtherInputs; + //! Includes watch only addresses which match the ISMINE_WATCH_PUBKEY criteria + bool fAllowWatchOnly; CCoinControl() { @@ -24,6 +26,7 @@ public: { destChange = CNoDestination(); fAllowOtherInputs = false; + fAllowWatchOnly = false; setSelected.clear(); } diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 8d8893387..199a93456 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2367,15 +2367,20 @@ UniValue fundrawtransaction(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() != 1) + if (fHelp || params.size() < 1 || params.size() > 2) throw runtime_error( - "fundrawtransaction \"hexstring\"\n" + "fundrawtransaction \"hexstring\" includeWatching\n" "\nAdd inputs to a transaction until it has enough in value to meet its out value.\n" "This will not modify existing inputs, and will add one change output to the outputs.\n" "Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n" "The inputs added will not be signed, use signrawtransaction for that.\n" + "Note that all existing inputs must have their previous output transaction be in the wallet.\n" + "Note that all inputs selected must be of standard form and P2SH scripts must be" + "in the wallet using importaddress or addmultisigaddress (to calculate fees).\n" + "Only pay-to-pubkey, multisig, and P2SH versions thereof are currently supported for watch-only\n" "\nArguments:\n" - "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" + "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" + "2. includeWatching (boolean, optional, default false) Also select inputs which are watch only\n" "\nResult:\n" "{\n" " \"hex\": \"value\", (string) The resulting raw transaction (hex-encoded string)\n" @@ -2394,18 +2399,22 @@ UniValue fundrawtransaction(const UniValue& params, bool fHelp) + HelpExampleCli("sendrawtransaction", "\"signedtransactionhex\"") ); - RPCTypeCheck(params, boost::assign::list_of(UniValue::VSTR)); + RPCTypeCheck(params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)); // parse hex string from parameter CTransaction origTx; if (!DecodeHexTx(origTx, params[0].get_str())) throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + bool includeWatching = false; + if (params.size() > 1) + includeWatching = true; + CMutableTransaction tx(origTx); CAmount nFee; string strFailReason; int nChangePos = -1; - if(!pwalletMain->FundTransaction(tx, nFee, nChangePos, strFailReason)) + if(!pwalletMain->FundTransaction(tx, nFee, nChangePos, strFailReason, includeWatching)) throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason); UniValue result(UniValue::VOBJ); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c11800863..5bc34f9ed 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1524,7 +1524,9 @@ void CWallet::AvailableCoins(vector& vCoins, bool fOnlyConfirmed, const if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO && !IsLockedCoin((*it).first, i) && (pcoin->vout[i].nValue > 0 || fIncludeZeroValue) && (!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected((*it).first, i))) - vCoins.push_back(COutput(pcoin, i, nDepth, (mine & ISMINE_SPENDABLE) != ISMINE_NO)); + vCoins.push_back(COutput(pcoin, i, nDepth, + ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || + (coinControl && coinControl->fAllowWatchOnly && (mine & ISMINE_WATCH_PUBKEY) != ISMINE_NO))); } } } @@ -1740,7 +1742,7 @@ bool CWallet::SelectCoins(const CAmount& nTargetValue, set vecSend; @@ -1753,6 +1755,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount &nFeeRet, int& nC CCoinControl coinControl; coinControl.fAllowOtherInputs = true; + coinControl.fAllowWatchOnly = includeWatching; BOOST_FOREACH(const CTxIn& txin, tx.vin) coinControl.Select(txin.prevout); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 003266ba1..faa509fc1 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -630,7 +630,7 @@ public: CAmount GetWatchOnlyBalance() const; CAmount GetUnconfirmedWatchOnlyBalance() const; CAmount GetImmatureWatchOnlyBalance() const; - bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosRet, std::string& strFailReason); + bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosRet, std::string& strFailReason, bool includeWatching); bool CreateTransaction(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosRet, std::string& strFailReason, const CCoinControl *coinControl = NULL, bool sign = true); bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey); From 428a898acd37e1c0afa21623a8fe5728859067be Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 8 Aug 2015 09:26:07 -0700 Subject: [PATCH 09/10] SQUASH "Add have-pubkey distinction to ISMINE flags" --- src/wallet/wallet_ismine.cpp | 4 ++-- src/wallet/wallet_ismine.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wallet/wallet_ismine.cpp b/src/wallet/wallet_ismine.cpp index 0303cbb2f..d27b1531e 100644 --- a/src/wallet/wallet_ismine.cpp +++ b/src/wallet/wallet_ismine.cpp @@ -41,7 +41,7 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey) txnouttype whichType; if (!Solver(scriptPubKey, whichType, vSolutions)) { if (keystore.HaveWatchOnly(scriptPubKey)) - return ISMINE_WATCH_NOPUBKEY; + return ISMINE_WATCH_UNSOLVABLE; return ISMINE_NO; } @@ -89,7 +89,7 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey) if (keystore.HaveWatchOnly(scriptPubKey)) { // TODO: This could be optimized some by doing some work after the above solver CScript scriptSig; - return ProduceSignature(DummySignatureCreator(&keystore), scriptPubKey, scriptSig) ? ISMINE_WATCH_PUBKEY : ISMINE_WATCH_NOPUBKEY; + return ProduceSignature(DummySignatureCreator(&keystore), scriptPubKey, scriptSig) ? ISMINE_WATCH_SOLVABLE : ISMINE_WATCH_UNSOLVABLE; } return ISMINE_NO; } diff --git a/src/wallet/wallet_ismine.h b/src/wallet/wallet_ismine.h index 12afad175..ec9dcddd5 100644 --- a/src/wallet/wallet_ismine.h +++ b/src/wallet/wallet_ismine.h @@ -17,10 +17,10 @@ enum isminetype { ISMINE_NO = 0, //! Indicates that we dont know how to create a scriptSig that would solve this if we were given the appropriate private keys - ISMINE_WATCH_NOPUBKEY = 1, + ISMINE_WATCH_UNSOLVABLE = 1, //! Indicates that we know how to create a scriptSig that would solve this if we were given the appropriate private keys - ISMINE_WATCH_PUBKEY = 2, - ISMINE_WATCH_ONLY = ISMINE_WATCH_NOPUBKEY | ISMINE_WATCH_PUBKEY, + ISMINE_WATCH_SOLVABLE = 2, + ISMINE_WATCH_ONLY = ISMINE_WATCH_SOLVABLE | ISMINE_WATCH_UNSOLVABLE, ISMINE_SPENDABLE = 4, ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE }; From d04285430d1b54b3ce3d50ffa67b6098157e7c14 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 8 Aug 2015 09:27:19 -0700 Subject: [PATCH 10/10] SQUASH "Implement watchonly support in fundrawtransaction" --- src/coincontrol.h | 2 +- src/wallet/wallet.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coincontrol.h b/src/coincontrol.h index 74d524c0a..bc965f9e1 100644 --- a/src/coincontrol.h +++ b/src/coincontrol.h @@ -14,7 +14,7 @@ public: CTxDestination destChange; //! If false, allows unselected inputs, but requires all selected inputs be used bool fAllowOtherInputs; - //! Includes watch only addresses which match the ISMINE_WATCH_PUBKEY criteria + //! Includes watch only addresses which match the ISMINE_WATCH_SOLVABLE criteria bool fAllowWatchOnly; CCoinControl() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5bc34f9ed..8b7231b6e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1526,7 +1526,7 @@ void CWallet::AvailableCoins(vector& vCoins, bool fOnlyConfirmed, const (!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected((*it).first, i))) vCoins.push_back(COutput(pcoin, i, nDepth, ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || - (coinControl && coinControl->fAllowWatchOnly && (mine & ISMINE_WATCH_PUBKEY) != ISMINE_NO))); + (coinControl && coinControl->fAllowWatchOnly && (mine & ISMINE_WATCH_SOLVABLE) != ISMINE_NO))); } } }