diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index 80e2c0b05..8e22ed397 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -14,6 +14,7 @@ zcash_gtest_SOURCES = \ gtest/test_merkletree.cpp \ gtest/test_circuit.cpp \ gtest/test_txid.cpp \ + gtest/test_wallet_zkeys.cpp \ gtest/test_libzcash_utils.cpp \ gtest/test_proofs.cpp diff --git a/src/gtest/test_wallet_zkeys.cpp b/src/gtest/test_wallet_zkeys.cpp new file mode 100644 index 000000000..d812c657d --- /dev/null +++ b/src/gtest/test_wallet_zkeys.cpp @@ -0,0 +1,140 @@ +#include + +#include "zcash/Address.hpp" +#include "wallet/wallet.h" +#include "wallet/walletdb.h" +#include "util.h" + +#include + +/** + * This test covers methods on CWallet + * GenerateNewZKey() + * AddZKey() + * LoadZKey() + * LoadZKeyMetadata() + */ +TEST(wallet_zkeys_tests, store_and_load_zkeys) { + SelectParams(CBaseChainParams::MAIN); + + CWallet wallet; + + // wallet should be empty + std::set addrs; + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(0, addrs.size()); + + // wallet should have one key + CZCPaymentAddress paymentAddress = wallet.GenerateNewZKey(); + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(1, addrs.size()); + + // verify wallet has spending key for the address + auto addr = paymentAddress.Get(); + ASSERT_TRUE(wallet.HaveSpendingKey(addr)); + + // manually add new spending key to wallet + auto sk = libzcash::SpendingKey::random(); + ASSERT_TRUE(wallet.AddZKey(sk)); + + // verify wallet did add it + addr = sk.address(); + ASSERT_TRUE(wallet.HaveSpendingKey(addr)); + + // verify spending key stored correctly + libzcash::SpendingKey keyOut; + wallet.GetSpendingKey(addr, keyOut); + ASSERT_EQ(sk, keyOut); + + // verify there are two keys + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(2, addrs.size()); + ASSERT_EQ(1, addrs.count(addr)); + + // Load a third key into the wallet + sk = libzcash::SpendingKey::random(); + ASSERT_TRUE(wallet.LoadZKey(sk)); + + // attach metadata to this third key + addr = sk.address(); + int64_t now = GetTime(); + CKeyMetadata meta(now); + ASSERT_TRUE(wallet.LoadZKeyMetadata(addr, meta)); + + // check metadata is the same + CKeyMetadata m= wallet.mapZKeyMetadata[addr]; + ASSERT_EQ(m.nCreateTime, now); +} + +/** + * This test covers methods on CWalletDB + * WriteZKey() + */ +TEST(wallet_zkeys_tests, write_zkey_direct_to_db) { + SelectParams(CBaseChainParams::TESTNET); + + // Get temporary and unique path for file. + // Note: / operator to append paths + boost::filesystem::path temp = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path(); + const std::string path = temp.native(); + + bool fFirstRun; + CWallet wallet(path); + ASSERT_EQ(DB_LOAD_OK, wallet.LoadWallet(fFirstRun)); + + // No default CPubKey set + ASSERT_TRUE(fFirstRun); + + // wallet should be empty + std::set addrs; + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(0, addrs.size()); + + // Add random key to the wallet + auto paymentAddress = wallet.GenerateNewZKey(); + + // wallet should have one key + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(1, addrs.size()); + + // create random key and add it to database directly, bypassing wallet + auto sk = libzcash::SpendingKey::random(); + auto addr = sk.address(); + int64_t now = GetTime(); + CKeyMetadata meta(now); + CWalletDB db(path); + db.WriteZKey(addr, sk, meta); + + // wallet should not be aware of key + ASSERT_FALSE(wallet.HaveSpendingKey(addr)); + + // wallet sees one key + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(1, addrs.size()); + + // wallet should have default metadata for addr with null createtime + CKeyMetadata m = wallet.mapZKeyMetadata[addr]; + ASSERT_EQ(m.nCreateTime, 0); + ASSERT_NE(m.nCreateTime, now); + + // load the wallet again + ASSERT_EQ(DB_LOAD_OK, wallet.LoadWallet(fFirstRun)); + + // wallet can now see the spending key + ASSERT_TRUE(wallet.HaveSpendingKey(addr)); + + // check key is the same + libzcash::SpendingKey keyOut; + wallet.GetSpendingKey(addr, keyOut); + ASSERT_EQ(sk, keyOut); + + // wallet should have two keys + wallet.GetPaymentAddresses(addrs); + ASSERT_EQ(2, addrs.size()); + + // check metadata is now the same + m = wallet.mapZKeyMetadata[addr]; + ASSERT_EQ(m.nCreateTime, now); +} + diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 7efb7d74c..78ae7d930 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -97,7 +97,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "zcrawjoinsplit", 4 }, { "zcbenchmark", 1 }, { "zcbenchmark", 2 }, - { "getblocksubsidy", 0} + { "getblocksubsidy", 0}, + { "z_importkey", 1 } }; class CRPCConvertTable diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 1a8562b87..3ad88b2e0 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -381,7 +381,13 @@ static const CRPCCommand vRPCCommands[] = { "wallet", "zcrawkeygen", &zc_raw_keygen, true }, { "wallet", "zcrawjoinsplit", &zc_raw_joinsplit, true }, { "wallet", "zcrawreceive", &zc_raw_receive, true }, - { "wallet", "zcsamplejoinsplit", &zc_sample_joinsplit, true } + { "wallet", "zcsamplejoinsplit", &zc_sample_joinsplit, true }, + { "wallet", "z_getnewaddress", &z_getnewaddress, true }, + { "wallet", "z_listaddresses", &z_listaddresses, true }, + { "wallet", "z_exportkey", &z_exportkey, true }, + { "wallet", "z_importkey", &z_importkey, true }, + { "wallet", "z_exportwallet", &z_exportwallet, true }, + { "wallet", "z_importwallet", &z_importwallet, true } #endif // ENABLE_WALLET }; diff --git a/src/rpcserver.h b/src/rpcserver.h index d90564891..0e8b60b7b 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -243,6 +243,13 @@ extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool extern json_spirit::Value getblocksubsidy(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value z_exportkey(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp +extern json_spirit::Value z_importkey(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp +extern json_spirit::Value z_getnewaddress(const json_spirit::Array& params, bool fHelp); // in rpcwallet.cpp +extern json_spirit::Value z_listaddresses(const json_spirit::Array& params, bool fHelp); // in rpcwallet.cpp +extern json_spirit::Value z_exportwallet(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp +extern json_spirit::Value z_importwallet(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp + // in rest.cpp extern bool HTTPReq_REST(AcceptedConnection *conn, const std::string& strURI, diff --git a/src/test/rpc_wallet_tests.cpp b/src/test/rpc_wallet_tests.cpp index 4d5e92cbd..ada3020b6 100644 --- a/src/test/rpc_wallet_tests.cpp +++ b/src/test/rpc_wallet_tests.cpp @@ -11,8 +11,14 @@ #include "test/test_bitcoin.h" +#include "zcash/Address.hpp" + +#include +#include + #include #include +#include using namespace std; using namespace json_spirit; @@ -218,4 +224,203 @@ BOOST_AUTO_TEST_CASE(rpc_wallet) BOOST_CHECK(CBitcoinAddress(arr[0].get_str()).Get() == demoAddress.Get()); } -BOOST_AUTO_TEST_SUITE_END() +/* + * This test covers RPC command z_exportwallet + */ +BOOST_AUTO_TEST_CASE(rpc_wallet_z_exportwallet) +{ + LOCK2(cs_main, pwalletMain->cs_wallet); + + // wallet should be empty + std::set addrs; + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==0); + + // wallet should have one key + CZCPaymentAddress paymentAddress = pwalletMain->GenerateNewZKey(); + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==1); + + BOOST_CHECK_THROW(CallRPC("z_exportwallet"), runtime_error); + + + boost::filesystem::path temp = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path(); + const std::string path = temp.native(); + + BOOST_CHECK_NO_THROW(CallRPC(string("z_exportwallet ") + path)); + + auto addr = paymentAddress.Get(); + libzcash::SpendingKey key; + BOOST_CHECK(pwalletMain->GetSpendingKey(addr, key)); + + std::string s1 = paymentAddress.ToString(); + std::string s2 = CZCSpendingKey(key).ToString(); + + // There's no way to really delete a private key so we will read in the + // exported wallet file and search for the spending key and payment address. + + EnsureWalletIsUnlocked(); + + ifstream file; + file.open(path.c_str(), std::ios::in | std::ios::ate); + BOOST_CHECK(file.is_open()); + bool fVerified = false; + int64_t nFilesize = std::max((int64_t)1, (int64_t)file.tellg()); + file.seekg(0, file.beg); + while (file.good()) { + std::string line; + std::getline(file, line); + if (line.empty() || line[0] == '#') + continue; + if (line.find(s1) != std::string::npos && line.find(s2) != std::string::npos) { + fVerified = true; + break; + } + } + BOOST_CHECK(fVerified); +} + + +/* + * This test covers RPC command z_importwallet + */ +BOOST_AUTO_TEST_CASE(rpc_wallet_z_importwallet) +{ + LOCK2(cs_main, pwalletMain->cs_wallet); + + // error if no args + BOOST_CHECK_THROW(CallRPC("z_importwallet"), runtime_error); + + // create a random key locally + auto testSpendingKey = libzcash::SpendingKey::random(); + auto testPaymentAddress = testSpendingKey.address(); + std::string testAddr = CZCPaymentAddress(testPaymentAddress).ToString(); + std::string testKey = CZCSpendingKey(testSpendingKey).ToString(); + + // create test data using the random key + std::string format_str = "# Wallet dump created by Zcash v0.11.2.0.z8-9155cc6-dirty (2016-08-11 11:37:00 -0700)\n" + "# * Created on 2016-08-12T21:55:36Z\n" + "# * Best block at time of backup was 0 (0de0a3851fef2d433b9b4f51d4342bdd24c5ddd793eb8fba57189f07e9235d52),\n" + "# mined on 2009-01-03T18:15:05Z\n" + "\n" + "# Zkeys\n" + "\n" + "%s 2016-08-12T21:55:36Z # zaddr=%s\n" + "\n" + "\n# End of dump"; + + boost::format formatobject(format_str); + std::string testWalletDump = (formatobject % testKey % testAddr).str(); + + // write test data to file + boost::filesystem::path temp = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path(); + const std::string path = temp.native(); + std::ofstream file(path); + file << testWalletDump; + file << std::flush; + + // wallet should currently be empty + std::set addrs; + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==0); + + // import test data from file into wallet + BOOST_CHECK_NO_THROW(CallRPC(string("z_importwallet ") + path)); + + // wallet should now have one zkey + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==1); + + // check that we have the spending key for the address + CZCPaymentAddress address(testAddr); + auto addr = address.Get(); + BOOST_CHECK(pwalletMain->HaveSpendingKey(addr)); + + // Verify the spending key is the same as the test data + libzcash::SpendingKey k; + BOOST_CHECK(pwalletMain->GetSpendingKey(addr, k)); + CZCSpendingKey spendingkey(k); + BOOST_CHECK_EQUAL(testKey, spendingkey.ToString()); +} + + +/* + * This test covers RPC commands z_listaddresses, z_importkey, z_exportkey + */ +BOOST_AUTO_TEST_CASE(rpc_wallet_z_importexport) +{ + LOCK2(cs_main, pwalletMain->cs_wallet); + Value retValue; + int n1 = 1000; // number of times to import/export + int n2 = 1000; // number of addresses to create and list + + // error if no args + BOOST_CHECK_THROW(CallRPC("z_importkey"), runtime_error); + BOOST_CHECK_THROW(CallRPC("z_exportkey"), runtime_error); + + // wallet should currently be empty + std::set addrs; + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==0); + + // verify import and export key + for (int i = 0; i < n1; i++) { + // create a random key locally + auto testSpendingKey = libzcash::SpendingKey::random(); + auto testPaymentAddress = testSpendingKey.address(); + std::string testAddr = CZCPaymentAddress(testPaymentAddress).ToString(); + std::string testKey = CZCSpendingKey(testSpendingKey).ToString(); + BOOST_CHECK_NO_THROW(CallRPC(string("z_importkey ") + testKey)); + BOOST_CHECK_NO_THROW(retValue = CallRPC(string("z_exportkey ") + testAddr)); + BOOST_CHECK_EQUAL(retValue.get_str(), testKey); + } + + // Verify we can list the keys imported + BOOST_CHECK_NO_THROW(retValue = CallRPC("z_listaddresses")); + Array arr = retValue.get_array(); + BOOST_CHECK(arr.size() == n1); + + // Put addresses into a set + std::unordered_set myaddrs; + for (Value element : arr) { + myaddrs.insert(element.get_str()); + } + + // Make new addresses for the set + for (int i=0; iGenerateNewZKey()).ToString()); + } + + // Verify number of addresses stored in wallet is n1+n2 + int numAddrs = myaddrs.size(); + BOOST_CHECK(numAddrs == n1+n2); + pwalletMain->GetPaymentAddresses(addrs); + BOOST_CHECK(addrs.size()==numAddrs); + + // Ask wallet to list addresses + BOOST_CHECK_NO_THROW(retValue = CallRPC("z_listaddresses")); + arr = retValue.get_array(); + BOOST_CHECK(arr.size() == numAddrs); + + // Create a set from them + std::unordered_set listaddrs; + for (Value element : arr) { + listaddrs.insert(element.get_str()); + } + + // Verify the two sets of addresses are the same + BOOST_CHECK(listaddrs.size() == numAddrs); + BOOST_CHECK(myaddrs == listaddrs); + + // Add one more address + BOOST_CHECK_NO_THROW(retValue = CallRPC("z_getnewaddress")); + std::string newaddress = retValue.get_str(); + CZCPaymentAddress pa(newaddress); + auto newAddr = pa.Get(); + BOOST_CHECK(pwalletMain->HaveSpendingKey(newAddr)); +} + + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index ab951d1d7..61aa4da50 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -27,6 +27,10 @@ using namespace std; void EnsureWalletIsUnlocked(); bool EnsureWalletIsAvailable(bool avoidException); +Value dumpwallet_impl(const Array& params, bool fHelp, bool fDumpZKeys); +Value importwallet_impl(const Array& params, bool fHelp, bool fImportZKeys); + + std::string static EncodeDumpTime(int64_t nTime) { return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime); } @@ -217,6 +221,29 @@ Value importaddress(const Array& params, bool fHelp) return Value::null; } +Value z_importwallet(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + + if (fHelp || params.size() != 1) + throw runtime_error( + "z_importwallet \"filename\"\n" + "\nImports taddr and zaddr keys from a wallet export file (see z_exportwallet).\n" + "\nArguments:\n" + "1. \"filename\" (string, required) The wallet file\n" + "\nExamples:\n" + "\nDump the wallet\n" + + HelpExampleCli("z_exportwallet", "\"test\"") + + "\nImport the wallet\n" + + HelpExampleCli("z_importwallet", "\"test\"") + + "\nImport using the json rpc call\n" + + HelpExampleRpc("z_importwallet", "\"test\"") + ); + + return importwallet_impl(params, fHelp, true); +} + Value importwallet(const Array& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) @@ -225,7 +252,7 @@ Value importwallet(const Array& params, bool fHelp) if (fHelp || params.size() != 1) throw runtime_error( "importwallet \"filename\"\n" - "\nImports keys from a wallet dump file (see dumpwallet).\n" + "\nImports taddr keys from a wallet dump file (see dumpwallet).\n" "\nArguments:\n" "1. \"filename\" (string, required) The wallet file\n" "\nExamples:\n" @@ -237,6 +264,11 @@ Value importwallet(const Array& params, bool fHelp) + HelpExampleRpc("importwallet", "\"test\"") ); + return importwallet_impl(params, fHelp, false); +} + +Value importwallet_impl(const Array& params, bool fHelp, bool fImportZKeys) +{ LOCK2(cs_main, pwalletMain->cs_wallet); EnsureWalletIsUnlocked(); @@ -265,6 +297,34 @@ Value importwallet(const Array& params, bool fHelp) boost::split(vstr, line, boost::is_any_of(" ")); if (vstr.size() < 2) continue; + + // Let's see if the address is a valid Zcash spending key + if (fImportZKeys) { + try { + CZCSpendingKey spendingkey(vstr[0]); + libzcash::SpendingKey key = spendingkey.Get(); + libzcash::PaymentAddress addr = key.address(); + if (pwalletMain->HaveSpendingKey(addr)) { + LogPrintf("Skipping import of zaddr %s (key already present)\n", CZCPaymentAddress(addr).ToString()); + continue; + } + int64_t nTime = DecodeDumpTime(vstr[1]); + LogPrintf("Importing zaddr %s...\n", CZCPaymentAddress(addr).ToString()); + if (!pwalletMain->AddZKey(key)) { + // Something went wrong + fGood = false; + continue; + } + // Successfully imported zaddr. Now import the metadata. + pwalletMain->mapZKeyMetadata[addr].nCreateTime = nTime; + continue; + } + catch (const std::runtime_error &e) { + LogPrintf("Importing detected an error: %s\n", e.what()); + // Not a valid spending key, so carry on and see if it's a Bitcoin style address. + } + } + CBitcoinSecret vchSecret; if (!vchSecret.SetString(vstr[0])) continue; @@ -359,15 +419,35 @@ Value dumpprivkey(const Array& params, bool fHelp) } -Value dumpwallet(const Array& params, bool fHelp) + +Value z_exportwallet(const Array& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) return Value::null; + if (fHelp || params.size() != 1) + throw runtime_error( + "z_exportwallet \"filename\"\n" + "\nExports all wallet keys, for taddr and zaddr, in a human-readable format.\n" + "\nArguments:\n" + "1. \"filename\" (string, required) The filename\n" + "\nExamples:\n" + + HelpExampleCli("z_exportwallet", "\"test\"") + + HelpExampleRpc("z_exportwallet", "\"test\"") + ); + + return dumpwallet_impl(params, fHelp, true); +} + +Value dumpwallet(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + if (fHelp || params.size() != 1) throw runtime_error( "dumpwallet \"filename\"\n" - "\nDumps all wallet keys in a human-readable format.\n" + "\nDumps taddr wallet keys in a human-readable format.\n" "\nArguments:\n" "1. \"filename\" (string, required) The filename\n" "\nExamples:\n" @@ -375,6 +455,11 @@ Value dumpwallet(const Array& params, bool fHelp) + HelpExampleRpc("dumpwallet", "\"test\"") ); + return dumpwallet_impl(params, fHelp, false); +} + +Value dumpwallet_impl(const Array& params, bool fHelp, bool fDumpZKeys) +{ LOCK2(cs_main, pwalletMain->cs_wallet); EnsureWalletIsUnlocked(); @@ -398,7 +483,7 @@ Value dumpwallet(const Array& params, bool fHelp) std::sort(vKeyBirth.begin(), vKeyBirth.end()); // produce output - file << strprintf("# Wallet dump created by Bitcoin %s (%s)\n", CLIENT_BUILD, CLIENT_DATE); + file << strprintf("# Wallet dump created by Zcash %s (%s)\n", CLIENT_BUILD, CLIENT_DATE); file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime())); file << strprintf("# * Best block at time of backup was %i (%s),\n", chainActive.Height(), chainActive.Tip()->GetBlockHash().ToString()); file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime())); @@ -419,7 +504,123 @@ Value dumpwallet(const Array& params, bool fHelp) } } file << "\n"; + + if (fDumpZKeys) { + std::set addresses; + pwalletMain->GetPaymentAddresses(addresses); + file << "\n"; + file << "# Zkeys\n"; + file << "\n"; + for (auto addr : addresses ) { + libzcash::SpendingKey key; + if (pwalletMain->GetSpendingKey(addr, key)) { + std::string strTime = EncodeDumpTime(pwalletMain->mapZKeyMetadata[addr].nCreateTime); + file << strprintf("%s %s # zaddr=%s\n", CZCSpendingKey(key).ToString(), strTime, CZCPaymentAddress(addr).ToString()); + } + } + file << "\n"; + } + file << "# End of dump\n"; file.close(); return Value::null; } + + +Value z_importkey(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + + if (fHelp || params.size() < 1 || params.size() > 3) + throw runtime_error( + "z_importkey \"zkey\" ( \"label\" rescan )\n" + "\nAdds a zkey (as returned by z_exportkey) to your wallet.\n" + "\nArguments:\n" + "1. \"zkey\" (string, required) The zkey (see z_exportkey)\n" + "2. 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" + "\nExport a zkey\n" + + HelpExampleCli("z_exportkey", "\"myaddress\"") + + "\nImport the zkey with rescan\n" + + HelpExampleCli("z_importkey", "\"mykey\"") + + "\nImport using a label and without rescan\n" + + HelpExampleCli("z_importkey", "\"mykey\" \"testing\" false") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("z_importkey", "\"mykey\", \"testing\", false") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + // Whether to perform rescan after import + bool fRescan = true; + if (params.size() > 1) + fRescan = params[1].get_bool(); + + string strSecret = params[0].get_str(); + CZCSpendingKey spendingkey(strSecret); + auto key = spendingkey.Get(); + auto addr = key.address(); + + { + // Don't throw error in case a key is already there + if (pwalletMain->HaveSpendingKey(addr)) + return Value::null; + + pwalletMain->MarkDirty(); + + if (!pwalletMain-> AddZKey(key)) + throw JSONRPCError(RPC_WALLET_ERROR, "Error adding spending key to wallet"); + + pwalletMain->mapZKeyMetadata[addr].nCreateTime = 1; + + // We want to scan for transactions and notes + if (fRescan) { + pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); + } + } + + return Value::null; +} + + +Value z_exportkey(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + + if (fHelp || params.size() != 1) + throw runtime_error( + "z_exportkey \"zaddr\"\n" + "\nReveals the zkey corresponding to 'zaddr'.\n" + "Then the z_importkey can be used with this output\n" + "\nArguments:\n" + "1. \"zaddr\" (string, required) The zaddr for the private key\n" + "\nResult:\n" + "\"key\" (string) The private key\n" + "\nExamples:\n" + + HelpExampleCli("z_exportkey", "\"myaddress\"") + + HelpExampleCli("z_importkey", "\"mykey\"") + + HelpExampleRpc("z_exportkey", "\"myaddress\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + string strAddress = params[0].get_str(); + + CZCPaymentAddress address(strAddress); + auto addr = address.Get(); + + libzcash::SpendingKey k; + if (!pwalletMain->GetSpendingKey(addr, k)) + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold private zkey for this zaddr"); + + CZCSpendingKey spendingkey(k); + return spendingkey.ToString(); +} + diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index b9d588b9a..e37f56b50 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2769,3 +2769,61 @@ Value zc_raw_keygen(const json_spirit::Array& params, bool fHelp) result.push_back(Pair("zcviewingkey", viewing_hex)); return result; } + + +Value z_getnewaddress(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + + if (fHelp || params.size() > 1) + throw runtime_error( + "z_getnewaddress\n" + "\nReturns a new zaddr for receiving payments.\n" + "\nArguments:\n" + "\nResult:\n" + "\"zcashaddress\" (string) The new zaddr\n" + "\nExamples:\n" + + HelpExampleCli("z_getnewaddress", "") + + HelpExampleRpc("z_getnewaddress", "") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + CZCPaymentAddress pubaddr = pwalletMain->GenerateNewZKey(); + std::string result = pubaddr.ToString(); + return result; +} + + +Value z_listaddresses(const Array& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return Value::null; + + if (fHelp || params.size() > 1) + throw runtime_error( + "z_listaddresses\n" + "\nReturns the list of zaddr belonging to the wallet.\n" + "\nArguments:\n" + "\nResult:\n" + "[ (json array of string)\n" + " \"zaddr\" (string) a zaddr belonging to the wallet\n" + " ,...\n" + "]\n" + "\nExamples:\n" + + HelpExampleCli("z_listaddresses", "") + + HelpExampleRpc("z_listaddresses", "") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + Array ret; + std::set addresses; + pwalletMain->GetPaymentAddresses(addresses); + for (auto addr : addresses ) { + ret.push_back(CZCPaymentAddress(addr).ToString()); + } + return ret; +} + diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f8215c939..0f6eeff49 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -25,6 +25,7 @@ #include using namespace std; +using namespace libzcash; /** * Settings @@ -70,6 +71,47 @@ const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const return &(it->second); } +// Generate a new spending key and return its public payment address +CZCPaymentAddress CWallet::GenerateNewZKey() +{ + AssertLockHeld(cs_wallet); // mapZKeyMetadata + auto k = SpendingKey::random(); + auto addr = k.address(); + + // Check for collision, even though it is unlikely to ever occur + if (CCryptoKeyStore::HaveSpendingKey(addr)) + throw std::runtime_error("CWallet::GenerateNewZKey(): Collision detected"); + + // Create new metadata + int64_t nCreationTime = GetTime(); + mapZKeyMetadata[addr] = CKeyMetadata(nCreationTime); + + CZCPaymentAddress pubaddr(addr); + if (!AddZKey(k)) + throw std::runtime_error("CWallet::GenerateNewZKey(): AddZKey failed"); + return pubaddr; +} + +// Add spending key to keystore and persist to disk +bool CWallet::AddZKey(const libzcash::SpendingKey &key) +{ + AssertLockHeld(cs_wallet); // mapZKeyMetadata + auto addr = key.address(); + + if (!CCryptoKeyStore::AddSpendingKey(key)) + return false; + + if (!fFileBacked) + return true; + + if (!IsCrypted()) { + return CWalletDB(strWalletFile).WriteZKey(addr, + key, + mapZKeyMetadata[addr]); + } + return true; +} + CPubKey CWallet::GenerateNewKey() { AssertLockHeld(cs_wallet); // mapKeyMetadata @@ -149,11 +191,23 @@ bool CWallet::LoadKeyMetadata(const CPubKey &pubkey, const CKeyMetadata &meta) return true; } +bool CWallet::LoadZKeyMetadata(const PaymentAddress &addr, const CKeyMetadata &meta) +{ + AssertLockHeld(cs_wallet); // mapZKeyMetadata + mapZKeyMetadata[addr] = meta; + return true; +} + bool CWallet::LoadCryptedKey(const CPubKey &vchPubKey, const std::vector &vchCryptedSecret) { return CCryptoKeyStore::AddCryptedKey(vchPubKey, vchCryptedSecret); } +bool CWallet::LoadZKey(const libzcash::SpendingKey &key) +{ + return CCryptoKeyStore::AddSpendingKey(key); +} + bool CWallet::AddCScript(const CScript& redeemScript) { if (!CCryptoKeyStore::AddCScript(redeemScript)) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f84f857f5..0651c6404 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -18,6 +18,8 @@ #include "wallet/crypter.h" #include "wallet/wallet_ismine.h" #include "wallet/walletdb.h" +#include "zcash/Address.hpp" +#include "base58.h" #include #include @@ -486,6 +488,7 @@ public: std::set setKeyPool; std::map mapKeyMetadata; + std::map mapZKeyMetadata; typedef std::map MasterKeyMap; MasterKeyMap mapMasterKeys; @@ -595,6 +598,19 @@ public: void GetKeyBirthTimes(std::map &mapKeyBirth) const; + /** + * ZKeys + */ + //! Generates a new zaddr + CZCPaymentAddress GenerateNewZKey(); + //! Adds spending key to the store, and saves it to disk + bool AddZKey(const libzcash::SpendingKey &key); + //! Adds spending key to the store, without saving it to disk (used by LoadWallet) + bool LoadZKey(const libzcash::SpendingKey &key); + //! Load spending key metadata (used by LoadWallet) + bool LoadZKeyMetadata(const libzcash::PaymentAddress &addr, const CKeyMetadata &meta); + + /** * Increment the next transaction order id * @return next transaction order id diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 6f08ad681..ab1d364e4 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -110,6 +110,17 @@ bool CWalletDB::WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey) return Write(std::make_pair(std::string("mkey"), nID), kMasterKey, true); } +bool CWalletDB::WriteZKey(const libzcash::PaymentAddress& addr, const libzcash::SpendingKey& key, const CKeyMetadata &keyMeta) +{ + nWalletDBUpdated++; + + if (!Write(std::make_pair(std::string("zkeymeta"), addr), keyMeta)) + return false; + + // pair is: tuple_key("zkey", paymentaddress) --> secretkey + return Write(std::make_pair(std::string("zkey"), addr), key, false); +} + bool CWalletDB::WriteCScript(const uint160& hash, const CScript& redeemScript) { nWalletDBUpdated++; @@ -330,13 +341,15 @@ public: unsigned int nKeys; unsigned int nCKeys; unsigned int nKeyMeta; + unsigned int nZKeys; + unsigned int nZKeyMeta; bool fIsEncrypted; bool fAnyUnordered; int nFileVersion; vector vWalletUpgrade; CWalletScanState() { - nKeys = nCKeys = nKeyMeta = 0; + nKeys = nCKeys = nKeyMeta = nZKeys = nZKeyMeta = 0; fIsEncrypted = false; fAnyUnordered = false; nFileVersion = 0; @@ -429,6 +442,21 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, // so set the wallet birthday to the beginning of time. pwallet->nTimeFirstKey = 1; } + else if (strType == "zkey") + { + libzcash::PaymentAddress addr; + ssKey >> addr; + libzcash::SpendingKey key; + ssValue >> key; + + if (!pwallet->LoadZKey(key)) + { + strErr = "Error reading wallet database: LoadZKey failed"; + return false; + } + + wss.nZKeys++; + } else if (strType == "key" || strType == "wkey") { CPubKey vchPubKey; @@ -538,6 +566,18 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, (keyMeta.nCreateTime < pwallet->nTimeFirstKey)) pwallet->nTimeFirstKey = keyMeta.nCreateTime; } + else if (strType == "zkeymeta") + { + libzcash::PaymentAddress addr; + ssKey >> addr; + CKeyMetadata keyMeta; + ssValue >> keyMeta; + wss.nZKeyMeta++; + + pwallet->LoadZKeyMetadata(addr, keyMeta); + + // ignore earliest key creation time as taddr will exist before any zaddr + } else if (strType == "defaultkey") { ssValue >> pwallet->vchDefaultKey; @@ -601,6 +641,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, static bool IsKeyType(string strType) { return (strType== "key" || strType == "wkey" || + strType == "zkey" || strType == "mkey" || strType == "ckey"); } @@ -685,6 +726,10 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) LogPrintf("Keys: %u plaintext, %u encrypted, %u w/ metadata, %u total\n", wss.nKeys, wss.nCKeys, wss.nKeyMeta, wss.nKeys + wss.nCKeys); + // TODO: Keep track of encrypted ZKeys + LogPrintf("ZKeys: %u plaintext, -- encrypted, %u w/metadata, %u total\n", + wss.nZKeys, wss.nZKeyMeta, wss.nZKeys + 0); + // nTimeFirstKey is only reliable if all keys have metadata if ((wss.nKeys + wss.nCKeys) != wss.nKeyMeta) pwallet->nTimeFirstKey = 1; // 0 would be considered 'no value' diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index bc1a104b5..d261c9644 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -10,6 +10,7 @@ #include "wallet/db.h" #include "key.h" #include "keystore.h" +#include "zcash/Address.hpp" #include #include @@ -130,6 +131,9 @@ public: static bool Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKeys); static bool Recover(CDBEnv& dbenv, const std::string& filename); + /// Write spending key to wallet database, where key is payment address and value is spending key. + bool WriteZKey(const libzcash::PaymentAddress& addr, const libzcash::SpendingKey& key, const CKeyMetadata &keyMeta); + private: CWalletDB(const CWalletDB&); void operator=(const CWalletDB&);