Require backup of the emergency recovery phrase.

After 4.5.2, all wallets will be populated with an emergency
recovery phrase, and all future addresses will be derived from
the associated seed. To prevent potential loss of funds, we
require that the user explicitly invoke the `walletconfirmbackup`
RPC method to verify that they have backed up this seed.
This commit is contained in:
Kris Nuttycombe 2021-10-27 18:44:09 -06:00
parent 5760a639d2
commit 8bf4ec3b4a
13 changed files with 166 additions and 50 deletions

View File

@ -13,7 +13,7 @@ class WalletBroadcastTest(BitcoinTestFramework):
#do some -walletbroadcast tests
stop_nodes(self.nodes)
wait_bitcoinds()
self.nodes = start_nodes(3, self.options.tmpdir, [["-walletbroadcast=0"],["-walletbroadcast=0"],["-walletbroadcast=0"]])
self.nodes = start_nodes(3, self.options.tmpdir, [["-walletbroadcast=0"]] * 3)
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,0,2)

View File

@ -3,13 +3,16 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_true, start_nodes
class WalletImportExportTest (BitcoinTestFramework):
def setup_network(self, split=False):
num_nodes = 3
extra_args = [["-exportdir={}/export{}".format(self.options.tmpdir, i)] for i in range(num_nodes)]
extra_args = [([
"-exportdir={}/export{}".format(self.options.tmpdir, i),
] + (["-walletrequirebackup"] if i == 0 else [])) for i in range(num_nodes)]
self.nodes = start_nodes(num_nodes, self.options.tmpdir, extra_args)
def run_test(self):
@ -17,12 +20,28 @@ class WalletImportExportTest (BitcoinTestFramework):
privkey2 = self.nodes[2].z_exportkey(sapling_address2)
self.nodes[0].z_importkey(privkey2)
# test walletconfirmbackup
try:
self.nodes[0].getnewaddress()
except JSONRPCException as e:
errorString = e.error['message']
assert_equal("Error: Please acknowledge that you have backed up" in errorString, True)
try:
self.nodes[0].z_getnewaddress('sapling')
except JSONRPCException as e:
errorString = e.error['message']
assert_equal("Error: Please acknowledge that you have backed up" in errorString, True)
dump_path0 = self.nodes[0].z_exportwallet('walletdumpmnem')
(mnemonic, t_keys0, sprout_keys0, sapling_keys0) = parse_wallet_file(dump_path0)
self.nodes[0].walletconfirmbackup(mnemonic)
# Now that we've confirmed backup, we can generate addresses
sprout_address0 = self.nodes[0].z_getnewaddress('sprout')
sapling_address0 = self.nodes[0].z_getnewaddress('sapling')
# node 0 should have the keys
dump_path0 = self.nodes[0].z_exportwallet('walletdump')
(t_keys0, sprout_keys0, sapling_keys0) = parse_wallet_file(dump_path0)
(_, t_keys0, sprout_keys0, sapling_keys0) = parse_wallet_file(dump_path0)
sapling_line_lengths = [len(sapling_key0.split(' #')[0].split()) for sapling_key0 in sapling_keys0.splitlines()]
assert_equal(2, len(sapling_line_lengths), "Should have 2 sapling keys")
@ -35,7 +54,7 @@ class WalletImportExportTest (BitcoinTestFramework):
# node 1 should not have the keys
dump_path1 = self.nodes[1].z_exportwallet('walletdumpbefore')
(t_keys1, sprout_keys1, sapling_keys1) = parse_wallet_file(dump_path1)
(_, t_keys1, sprout_keys1, sapling_keys1) = parse_wallet_file(dump_path1)
assert_true(sprout_address0 not in sprout_keys1)
assert_true(sapling_address0 not in sapling_keys1)
@ -45,7 +64,7 @@ class WalletImportExportTest (BitcoinTestFramework):
# node 1 should now have the keys
dump_path1 = self.nodes[1].z_exportwallet('walletdumpafter')
(t_keys1, sprout_keys1, sapling_keys1) = parse_wallet_file(dump_path1)
(_, t_keys1, sprout_keys1, sapling_keys1) = parse_wallet_file(dump_path1)
assert_true(sprout_address0 in sprout_keys1)
assert_true(sapling_address0 in sapling_keys1)
@ -62,11 +81,13 @@ def parse_wallet_file(dump_path):
assert_true("Emergency Recovery Phrase" in file_lines[4], "Expected Emergency Recovery Phrase")
assert_true("language" in file_lines[5], "Expected mnemonic seed language")
assert_true("fingerprint" in file_lines[6], "Expected mnemonic seed fingerprint")
mnemonic = file_lines[4].split("=")[1].strip()
print(mnemonic)
(t_keys, i) = parse_wallet_file_lines(file_lines, 0)
(sprout_keys, i) = parse_wallet_file_lines(file_lines, i)
(sapling_keys, i) = parse_wallet_file_lines(file_lines, i)
return (t_keys, sprout_keys, sapling_keys)
return (mnemonic, t_keys, sprout_keys, sapling_keys)
def parse_wallet_file_lines(file_lines, i):
keys = []

View File

@ -729,6 +729,9 @@ public:
// Founders reward script expects a vector of 2-of-3 multisig addresses
vFoundersRewardAddress = { "t2FwcEhFdNXuFMv1tcYwaBJtYVtMj8b1uTg" };
assert(vFoundersRewardAddress.size() <= consensus.GetLastFoundersRewardBlockHeight(0));
// do not require the wallet backup to be confirmed in regtest mode
fRequireWalletBackup = false;
}
void UpdateNetworkUpgradeParameters(Consensus::UpgradeIndex idx, int nActivationHeight)

View File

@ -50,7 +50,7 @@ public:
* a regression test mode which is intended for private networks only. It has
* minimal difficulty to ensure that blocks can be found instantly.
*/
class CChainParams: public KeyConstants
class CChainParams: public KeyConstants
{
public:
const Consensus::Params& GetConsensus() const { return consensus; }
@ -62,6 +62,7 @@ public:
CAmount SproutValuePoolCheckpointBalance() const { return nSproutValuePoolCheckpointBalance; }
uint256 SproutValuePoolCheckpointBlockHash() const { return hashSproutValuePoolCheckpointBlock; }
bool ZIP209Enabled() const { return fZIP209Enabled; }
bool RequireWalletBackup() const { return fRequireWalletBackup; }
const CBlock& GenesisBlock() const { return genesis; }
/** Make miner wait to have peers to avoid wasting work */
@ -80,11 +81,11 @@ public:
/** Return the BIP70 network string (main, test or regtest) */
std::string NetworkIDString() const { return keyConstants.NetworkIDString(); }
const std::vector<CDNSSeedData>& DNSSeeds() const { return vSeeds; }
const std::vector<unsigned char>& Base58Prefix(Base58Type type) const {
return keyConstants.Base58Prefix(type);
const std::vector<unsigned char>& Base58Prefix(Base58Type type) const {
return keyConstants.Base58Prefix(type);
}
const std::string& Bech32HRP(Bech32Type type) const {
return keyConstants.Bech32HRP(type);
const std::string& Bech32HRP(Bech32Type type) const {
return keyConstants.Bech32HRP(type);
}
const std::vector<SeedSpec6>& FixedSeeds() const { return vFixedSeeds; }
const CCheckpointData& Checkpoints() const { return checkpointData; }
@ -121,6 +122,7 @@ protected:
CAmount nSproutValuePoolCheckpointBalance = 0;
uint256 hashSproutValuePoolCheckpointBlock;
bool fZIP209Enabled = false;
bool fRequireWalletBackup = true;
};
/**

View File

@ -5,7 +5,7 @@
#ifndef ZCASH_KEY_CONSTANTS_H
#define ZCASH_KEY_CONSTANTS_H
class KeyConstants
class KeyConstants
{
public:
enum Base58Type {

View File

@ -75,6 +75,7 @@ enum RPCErrorCode
RPC_WALLET_WRONG_ENC_STATE = -15, //! Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.)
RPC_WALLET_ENCRYPTION_FAILED = -16, //! Failed to encrypt the wallet
RPC_WALLET_ALREADY_UNLOCKED = -17, //! Wallet is already unlocked
RPC_WALLET_BACKUP_REQUIRED = -18, //! User must acknowledge backup of the mnemonic seed.
};
std::string JSONRPCRequest(const std::string& strMethod, const UniValue& params, const UniValue& id);

View File

@ -37,6 +37,8 @@
#include <stdint.h>
#include <boost/assign/list_of.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/algorithm/string/erase.hpp>
#include <utf8.h>
#include <univalue.h>
@ -53,6 +55,7 @@ using namespace libzcash;
const std::string ADDR_TYPE_SPROUT = "sprout";
const std::string ADDR_TYPE_SAPLING = "sapling";
const bool DEFAULT_WALLET_REQUIRE_BACKUP = true;
extern UniValue TxJoinSplitToJSON(const CTransaction& tx);
@ -81,6 +84,16 @@ bool EnsureWalletIsAvailable(bool avoidException)
return true;
}
void EnsureWalletIsBackedUp(const CChainParams& params)
{
if (GetBoolArg("-walletrequirebackup", params.RequireWalletBackup()) && !pwalletMain->MnemonicVerified())
throw JSONRPCError(
RPC_WALLET_BACKUP_REQUIRED,
"Error: Please acknowledge that you have backed up the wallet's emergency recovery phrase "
"with walletconfirmbackup first."
);
}
void EnsureWalletIsUnlocked()
{
if (pwalletMain->IsLocked())
@ -163,6 +176,9 @@ UniValue getnewaddress(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
const CChainParams& chainparams = Params();
EnsureWalletIsBackedUp(chainparams);
if (!pwalletMain->IsLocked())
pwalletMain->TopUpKeyPool();
@ -175,7 +191,7 @@ UniValue getnewaddress(const UniValue& params, bool fHelp)
std::string dummy_account;
pwalletMain->SetAddressBook(keyID, dummy_account, "receive");
KeyIO keyIO(Params());
KeyIO keyIO(chainparams);
return keyIO.EncodeDestination(keyID);
}
@ -198,6 +214,9 @@ UniValue getrawchangeaddress(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
const CChainParams& chainparams = Params();
EnsureWalletIsBackedUp(chainparams);
if (!pwalletMain->IsLocked())
pwalletMain->TopUpKeyPool();
@ -210,7 +229,7 @@ UniValue getrawchangeaddress(const UniValue& params, bool fHelp)
CKeyID keyID = vchPubKey.GetID();
KeyIO keyIO(Params());
KeyIO keyIO(chainparams);
return keyIO.EncodeDestination(keyID);
}
@ -1586,6 +1605,8 @@ UniValue keypoolrefill(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsBackedUp(Params());
// 0 is interpreted by TopUpKeyPool() as the default keypool size given by -keypool
unsigned int kpSize = 0;
if (params.size() > 0) {
@ -1718,6 +1739,47 @@ UniValue walletpassphrasechange(const UniValue& params, bool fHelp)
return NullUniValue;
}
UniValue walletconfirmbackup(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() != 1)
throw runtime_error(
"walletconfirmbackup \"emergency recovery phrase\"\n"
"\nNotify the wallet that the user has backed up the emergency recovery phrase,\n"
"which can be obtained by making a call to z_exportwallet. The zcashd embedded wallet\n"
"requires confirmation that the emergency recovery phrase has been backed up before it\n"
"will permit new spending keys or addresses to be generated.\n"
"\nArguments:\n"
"1. \"emergency recovery phrase\" (string, required) The full recovery phrase returned as part\n"
" of the data returned by z_exportwallet. An error will be returned if the value provided\n"
" does not match the wallet's existing emergency recovery phrase.\n"
"\nExamples:\n"
+ HelpExampleCli("walletconfirmbackup", "\"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art\"")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
auto strMnemonicPhrase = params[0].get_str();
boost::erase_all(strMnemonicPhrase, "\"");
boost::trim(strMnemonicPhrase);
if (strMnemonicPhrase.length() > 0) {
if (!pwalletMain->VerifyMnemonicSeed(strMnemonicPhrase))
throw JSONRPCError(
RPC_WALLET_PASSPHRASE_INCORRECT,
"Error: The emergency recovery phrase entered was incorrect.");
} else {
throw runtime_error(
"walletconfirmbackup \"emergency recovery phrase\"\n"
"Notify the wallet that the user has backed up the emergency recovery phrase");
}
return NullUniValue;
}
UniValue walletlock(const UniValue& params, bool fHelp)
{
@ -2929,14 +2991,17 @@ UniValue z_getnewaddress(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
const CChainParams& chainparams = Params();
EnsureWalletIsUnlocked();
EnsureWalletIsBackedUp(chainparams);
auto addrType = defaultType;
if (params.size() > 0) {
addrType = params[0].get_str();
}
KeyIO keyIO(Params());
KeyIO keyIO(chainparams);
if (addrType == ADDR_TYPE_SPROUT) {
return keyIO.EncodePaymentAddress(pwalletMain->GenerateNewSproutZKey());
} else if (addrType == ADDR_TYPE_SAPLING) {
@ -4910,6 +4975,7 @@ static const CRPCCommand commands[] =
{ "wallet", "walletlock", &walletlock, true },
{ "wallet", "walletpassphrasechange", &walletpassphrasechange, true },
{ "wallet", "walletpassphrase", &walletpassphrase, true },
{ "wallet", "walletconfirmbackup", &walletconfirmbackup, true },
{ "wallet", "zcbenchmark", &zc_benchmark, true },
{ "wallet", "zcrawkeygen", &zc_raw_keygen, true },
{ "wallet", "zcrawjoinsplit", &zc_raw_joinsplit, true },

View File

@ -197,6 +197,11 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
*********************************/
BOOST_CHECK_NO_THROW(CallRPC("listaddressgroupings"));
/*********************************
* walletconfirmbackup
*********************************/
BOOST_CHECK_THROW(CallRPC(string("walletconfirmbackup \"badmnemonic\"")), runtime_error);
/*********************************
* getrawchangeaddress
*********************************/
@ -798,12 +803,6 @@ void CheckHaveAddr(const libzcash::PaymentAddress& addr) {
BOOST_AUTO_TEST_CASE(rpc_wallet_z_getnewaddress) {
using namespace libzcash;
if (!pwalletMain->HaveLegacyHDSeed()) {
// fake a legacy seed by creating a separate mnemonic seed
auto seed = MnemonicSeed::Random(1);
pwalletMain->LoadLegacyHDSeed(seed);
}
UniValue addr;
KeyIO keyIO(Params());
@ -1562,12 +1561,6 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_zkeys)
{
LOCK2(cs_main, pwalletMain->cs_wallet);
if (!pwalletMain->HaveLegacyHDSeed()) {
// fake a legacy seed by creating a separate mnemonic seed
auto seed = MnemonicSeed::Random(1);
pwalletMain->LoadLegacyHDSeed(seed);
}
UniValue retValue;
int n = 100;
@ -1625,12 +1618,6 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_sapzkeys)
{
LOCK2(cs_main, pwalletMain->cs_wallet);
if (!pwalletMain->HaveLegacyHDSeed()) {
// fake a legacy seed by creating a separate mnemonic seed
auto seed = MnemonicSeed::Random(1);
pwalletMain->LoadLegacyHDSeed(seed);
}
UniValue retValue;
int n = 100;
@ -1694,12 +1681,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_listunspent_parameters)
{
SelectParams(CBaseChainParams::TESTNET);
if (!pwalletMain->HaveLegacyHDSeed()) {
// fake a legacy seed by creating a separate mnemonic seed
auto seed = MnemonicSeed::Random(1);
pwalletMain->LoadLegacyHDSeed(seed);
}
LOCK2(cs_main, pwalletMain->cs_wallet);
UniValue retValue;

View File

@ -11,8 +11,10 @@ WalletTestingSetup::WalletTestingSetup(): TestingSetup()
bool fFirstRun;
pwalletMain = new CWallet(Params(), "wallet_test.dat");
pwalletMain->LoadWallet(fFirstRun);
if (!pwalletMain->HaveMnemonicSeed())
if (!pwalletMain->HaveMnemonicSeed()) {
pwalletMain->GenerateNewSeed();
pwalletMain->VerifyMnemonicSeed(pwalletMain->GetMnemonicSeed().value().GetMnemonic());
}
RegisterValidationInterface(pwalletMain);
RegisterWalletRPCCommands(tableRPC);

View File

@ -110,14 +110,13 @@ libzcash::SproutPaymentAddress CWallet::GenerateNewSproutZKey()
return addr;
}
// Generate a new Sapling spending key (generate a new account) based upon
// the legacy HD seed associated with this wallet and return its
// public payment address.
// Generate a new Sapling spending key as a child of the legacy Sapling account
// return its public payment address.
//
// The z_getnewaddress API must use the legacy HD seed, and fail if that seed
// is not present. When using legacy HD seeds, the account index is determined
// by trial of legacyHDChain.GetAccountCounter(); for unified addresses this must use
// valued derived from legacyHDChain.unifiedAccountCounter
// The z_getnewaddress API must use the mnemonic HD seed, and fail if that seed
// is not present. The account index is determined by trial of values of
// mnemonicHDChain.GetLegacySaplingKeyCounter() until one is found that produces
// a valid Sapling key.
SaplingPaymentAddress CWallet::GenerateNewLegacySaplingZKey() {
AssertLockHeld(cs_wallet);
@ -2351,6 +2350,31 @@ bool CWallet::SetCryptedMnemonicSeed(const uint256& seedFp, const std::vector<un
return false;
}
bool CWallet::VerifyMnemonicSeed(std::string mnemonic) {
LOCK(cs_wallet);
auto seed = GetMnemonicSeed();
if (seed.has_value() && seed.value().GetMnemonic() == mnemonic) {
if (!mnemonicHDChain.has_value()) {
mnemonicHDChain = CHDChain(seed.value().Fingerprint(), GetTime());
}
CHDChain& hdChain = mnemonicHDChain.value();
hdChain.SetMnemonicSeedBackupConfirmed();
// Update the persisted chain information
if (fFileBacked && !CWalletDB(strWalletFile).WriteMnemonicHDChain(hdChain)) {
throw std::runtime_error(
"CWallet::VerifyMnemonicSeed(): Writing HD chain model failed");
}
return true;
} else {
return false;
}
}
bool CWallet::MnemonicVerified() {
return mnemonicHDChain.has_value() && mnemonicHDChain.value().IsMnemonicSeedBackupConfirmed();
}
HDSeed CWallet::GetHDSeedForRPC() const {
auto seed = pwalletMain->GetMnemonicSeed();
if (!seed.has_value()) {
@ -4783,6 +4807,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug)
strUsage += HelpMessageOpt("-walletnotify=<cmd>", _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)"));
strUsage += HelpMessageOpt("-zapwallettxes=<mode>", _("Delete all wallet transactions and only recover those parts of the blockchain through -rescan on startup") +
" " + _("(1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data)"));
strUsage += HelpMessageOpt("-walletrequirebackup=false", _("Allow generation of new spending keys & addresses from the mnemonic seed, even if the backup of that seed has not yet been confirmed with `walletconfirmbackup`."));
if (showDebug)
{

View File

@ -806,8 +806,6 @@ protected:
/* the hd chain metadata for keys derived from the mnemonic seed */
std::optional<CHDChain> mnemonicHDChain;
/* the hd chain metadata for keys derived from the legacy seed */
std::optional<CHDChain> legacyHDChain;
/* the network ID string for the network for which this wallet was created */
std::string networkIdString;
@ -1306,6 +1304,10 @@ public:
bool SetMnemonicSeed(const MnemonicSeed& seed);
bool SetCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
/* Checks the wallet's seed against the specified mnemonic, and marks the
* wallet's seed as having been backed up if the phrases match. */
bool VerifyMnemonicSeed(std::string mnemonic);
bool MnemonicVerified();
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
bool LoadMnemonicSeed(const MnemonicSeed& seed);

View File

@ -51,6 +51,7 @@ private:
uint32_t accountCounter;
uint32_t legacyTKeyCounter;
uint32_t legacySaplingKeyCounter;
bool mnemonicSeedBackupConfirmed;
CHDChain() { SetNull(); }
@ -62,12 +63,13 @@ private:
accountCounter = 0;
legacyTKeyCounter = 0;
legacySaplingKeyCounter = 0;
mnemonicSeedBackupConfirmed = false;
}
public:
static const int VERSION_HD_BASE = 1;
static const int CURRENT_VERSION = VERSION_HD_BASE;
CHDChain(uint256 seedFpIn, int64_t nCreateTimeIn): nVersion(CHDChain::CURRENT_VERSION), seedFp(seedFpIn), nCreateTime(nCreateTimeIn), accountCounter(0), legacyTKeyCounter(0), legacySaplingKeyCounter(0) {}
CHDChain(uint256 seedFpIn, int64_t nCreateTimeIn): nVersion(CHDChain::CURRENT_VERSION), seedFp(seedFpIn), nCreateTime(nCreateTimeIn), accountCounter(0), legacyTKeyCounter(0), legacySaplingKeyCounter(0), mnemonicSeedBackupConfirmed(false) {}
ADD_SERIALIZE_METHODS;
@ -80,6 +82,7 @@ public:
READWRITE(accountCounter);
READWRITE(legacyTKeyCounter);
READWRITE(legacySaplingKeyCounter);
READWRITE(mnemonicSeedBackupConfirmed);
}
template <typename Stream>
@ -116,6 +119,14 @@ public:
void IncrementLegacySaplingKeyCounter() {
legacySaplingKeyCounter += 1;
}
void SetMnemonicSeedBackupConfirmed() {
mnemonicSeedBackupConfirmed = true;
}
bool IsMnemonicSeedBackupConfirmed() {
return mnemonicSeedBackupConfirmed;
}
};
/** Access to the wallet database */

View File

@ -42,10 +42,12 @@ EXPECTED_BOOST_INCLUDES=(
boost/algorithm/string.hpp
boost/algorithm/string/case_conv.hpp
boost/algorithm/string/classification.hpp
boost/algorithm/string/erase.hpp
boost/algorithm/string/join.hpp
boost/algorithm/string/predicate.hpp
boost/algorithm/string/replace.hpp
boost/algorithm/string/split.hpp
boost/algorithm/string/trim.hpp
boost/assert.hpp
boost/assign/list_of.hpp
boost/assign/std/vector.hpp