Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions src/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ static std::string g_sendStatus;
static char g_backupPath[512] = {};
static std::string g_backupStatus;
static int64 g_lastWalletBackup = 0;
static WalletRecoveryAudit g_recoveryAudit;
static int64 g_recoveryAuditTime = 0;
static bool g_walletSafetyLoaded = false;
static char g_participantPool[256] = {};
static char g_poolName[128] = {};
Expand Down Expand Up @@ -184,6 +186,15 @@ static void LoadWalletSafetyState()
g_walletSafetyLoaded = true;
}

static void RefreshRecoveryAudit(bool fForce=false)
{
int64 nNow = GetTime();
if (!fForce && g_recoveryAuditTime != 0 && nNow - g_recoveryAuditTime < 5)
return;
g_recoveryAudit = GetWalletRecoveryAudit();
g_recoveryAuditTime = nNow;
}

static int KeyPoolCount()
{
int n = 0;
Expand Down Expand Up @@ -267,7 +278,11 @@ static void RefreshWallet()
g_lastWalletRefresh = GetTime();
}

void MainFrameRepaint() { g_needRefresh = true; }
void MainFrameRepaint()
{
g_needRefresh = true;
g_recoveryAuditTime = 0;
}

// DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is
// needed by builds that have no GUI at all.
Expand Down Expand Up @@ -967,6 +982,7 @@ static void RestoreThread(std::string strPhrase)
"Check the words and their order.", nDerived);
}
g_restoreRunning.store(false);
g_recoveryAuditTime = 0;
g_needRefresh = true;
}

Expand Down Expand Up @@ -1013,6 +1029,7 @@ static void DrawCreatePhraseDialog()
if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError))
{
TopUpKeyPool();
g_recoveryAuditTime = 0;
g_phraseStatus = "Recovery phrase created. New addresses come from it.";
}
else
Expand Down Expand Up @@ -1133,8 +1150,9 @@ static void DrawWalletSafetyDialog()
{
if (!g_showWalletSafety) return;
LoadWalletSafetyState();
RefreshRecoveryAudit();

ImGui::SetNextWindowSize(ImVec2(650.0f, 390.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(650.0f, 450.0f), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (ImGui::Begin("Wallet Safety", &g_showWalletSafety,
Expand Down Expand Up @@ -1221,6 +1239,38 @@ static void DrawWalletSafetyDialog()
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
"No recovery phrase. Only a file backup can rebuild this wallet.");

ImGui::Text("Phrase-backed spendable balance: %s BTF",
FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str());
ImGui::Text("Wallet.dat-only spendable balance: %s BTF",
FmtMoney(g_recoveryAudit.nLegacyCredit).c_str());
ImGui::Text("Phrase-backed immature mining rewards: %s BTF",
FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str());
ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF",
FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str());
if (!g_recoveryAudit.fDeriveComplete)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
ImGui::TextWrapped("%s", g_recoveryAudit.strDeriveError.c_str());
ImGui::PopStyleColor();
}
if (g_recoveryAudit.nLegacyCredit > 0 ||
g_recoveryAudit.nLegacyImmatureCredit > 0)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
ImGui::TextWrapped(
"Some coins are on keys the phrase does not reproduce. "
"Keep wallet.dat backups until that balance has been moved to a "
"phrase-backed address.");
ImGui::PopStyleColor();
}
else if (HaveHDSeed() &&
g_recoveryAudit.nRecoverableCredit +
g_recoveryAudit.nRecoverableImmatureCredit > 0)
{
ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f),
"All known wallet balance is covered by the phrase.");
}

if (!HaveHDSeed())
{
if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f)))
Expand Down
87 changes: 87 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3660,6 +3660,93 @@ bool BitcoinMiner(int nThreadId)
//


WalletRecoveryAudit GetWalletRecoveryAudit()
{
WalletRecoveryAudit audit;
set<vector<unsigned char> > setDerivedPubKeys;

CRITICAL_BLOCK(cs_keyPool)
{
audit.fHaveSeed = HaveHDSeed();
audit.nDerivedKnown = nHDNext;
if (audit.fHaveSeed)
{
string strError;
for (unsigned int i = 0; i < nHDNext; i++)
{
CKey key;
if (!DeriveHDKey(i, key, strError))
{
audit.fDeriveComplete = false;
audit.strDeriveError = strprintf("derivation failed at index %u: %s",
i, strError.c_str());
break;
}
setDerivedPubKeys.insert(key.GetPubKey());
}
}
}

CRITICAL_BLOCK(cs_mapWallet)
{
for (map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
CWalletTx& wtx = (*it).second;
if (!wtx.IsFinal() || wtx.fSpent)
continue;
bool fImmature = wtx.IsCoinBase() && wtx.GetBlocksToMaturity() > 0;

bool fTxRecoverable = false;
bool fTxLegacy = false;
for (int i = 0; i < (int)wtx.vout.size(); i++)
{
const CTxOut& txout = wtx.vout[i];
if (!txout.IsMine())
continue;

vector<unsigned char> vchPubKey;
bool fRecoverable = false;
if (audit.fHaveSeed && ExtractPubKey(txout.scriptPubKey, true, vchPubKey))
fRecoverable = setDerivedPubKeys.count(vchPubKey) > 0;

if (fRecoverable)
{
if (fImmature)
audit.nRecoverableImmatureCredit += txout.nValue;
else
audit.nRecoverableCredit += txout.nValue;
fTxRecoverable = true;
}
else
{
if (fImmature)
audit.nLegacyImmatureCredit += txout.nValue;
else
audit.nLegacyCredit += txout.nValue;
fTxLegacy = true;
}
}

if (fTxRecoverable)
{
if (fImmature)
audit.nRecoverableImmatureTx++;
else
audit.nRecoverableTx++;
}
if (fTxLegacy)
{
if (fImmature)
audit.nLegacyImmatureTx++;
else
audit.nLegacyTx++;
}
}
}

return audit;
}

int64 GetBalance()
{
int64 nStart, nEnd;
Expand Down
31 changes: 31 additions & 0 deletions src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,37 @@ extern CCriticalSection cs_mapTransactions;
bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv);
bool SendMessages(CNode* pto);
int64 GetBalance();
struct WalletRecoveryAudit
{
bool fHaveSeed;
bool fDeriveComplete;
unsigned int nDerivedKnown;
int nRecoverableTx;
int nLegacyTx;
int nRecoverableImmatureTx;
int nLegacyImmatureTx;
int64 nRecoverableCredit;
int64 nLegacyCredit;
int64 nRecoverableImmatureCredit;
int64 nLegacyImmatureCredit;
string strDeriveError;

WalletRecoveryAudit()
{
fHaveSeed = false;
fDeriveComplete = true;
nDerivedKnown = 0;
nRecoverableTx = 0;
nLegacyTx = 0;
nRecoverableImmatureTx = 0;
nLegacyImmatureTx = 0;
nRecoverableCredit = 0;
nLegacyCredit = 0;
nRecoverableImmatureCredit = 0;
nLegacyImmatureCredit = 0;
}
};
WalletRecoveryAudit GetWalletRecoveryAudit();
bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet);
bool CommitTransactionSpent(const CWalletTx& wtxNew);
bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
Expand Down
12 changes: 11 additions & 1 deletion src/main_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ static void PrintUsage()
printf(" /restoredepth=N (with /restorephrase: derive at least N\n");
printf(" addresses before giving up)\n");
printf(" /showderived=N (list the first N addresses a phrase\n");
printf(" produces, without installing it)\n");
printf(" installed in this wallet derives)\n");
printf(" /recoveryaudit (show how much spendable balance is\n");
printf(" covered by the recovery phrase;\n");
printf(" exits 2 when wallet.dat is still needed)\n");
printf(" /rescan (walk the chain for coins this wallet owns\n");
printf(" but never recorded, then exit)\n");
printf("\n");
Expand Down Expand Up @@ -385,6 +388,13 @@ int main(int argc, char* argv[])
return nRet;
}

if (arg(argc,argv,"/recoveryaudit") || arg(argc,argv,"-recoveryaudit"))
{
int nRet = CmdRecoveryAudit();
DBFlush(true);
return nRet;
}

if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase"))
{
int nRet = CmdNewPhrase();
Expand Down
36 changes: 36 additions & 0 deletions src/selftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ static int RunWalletHDSelfTest()

// The address the wallet was showing before any of this. It has to
// survive under a name that says it is not covered by the phrase.
std::vector<unsigned char> vchPreSeedKey = keyUser.GetPubKey();
std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey());

nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError),
Expand Down Expand Up @@ -449,6 +450,41 @@ static int RunWalletHDSelfTest()
}
nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1;
nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1;

// A recovery phrase is only useful if the wallet can tell the user
// what today's spendable balance would actually come back from it.
CKey keyAuditDerived;
if (!DeriveHDKey(0, keyAuditDerived, strError))
throw std::runtime_error("audit derivation failed: " + strError);

CWalletTx wtxLegacy;
wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG));
CWalletTx wtxDerived;
wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG));
CWalletTx wtxImmatureLegacy;
wtxImmatureLegacy.vin.push_back(CTxIn());
wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG));

CRITICAL_BLOCK(cs_mapWallet)
{
mapWallet.clear();
mapWallet[wtxLegacy.GetHash()] = wtxLegacy;
mapWallet[wtxDerived.GetHash()] = wtxDerived;
mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy;
}

WalletRecoveryAudit audit = GetWalletRecoveryAudit();
nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1;
nFail += Check(audit.nLegacyCredit == 5 * COIN,
"the recovery audit finds wallet.dat-only balance") ? 0 : 1;
nFail += Check(audit.nRecoverableCredit == 7 * COIN,
"the recovery audit finds phrase-backed balance") ? 0 : 1;
nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 1,
"the recovery audit counts legacy and phrase-backed transactions") ? 0 : 1;
nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN,
"the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1;
nFail += Check(audit.nLegacyImmatureTx == 1,
"the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1;
}
catch (const std::exception& e)
{
Expand Down
59 changes: 59 additions & 0 deletions src/walletcmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,65 @@ int CmdShowDerived(int nCount)
return 0;
}

int CmdRecoveryAudit()
{
AttachTerminal();

WalletRecoveryAudit audit = GetWalletRecoveryAudit();
int64 nTotal = audit.nRecoverableCredit + audit.nLegacyCredit;
int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit;

printf("Wallet recovery audit\n");
printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed");
if (audit.fHaveSeed)
printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown);
if (!audit.fDeriveComplete)
printf(" derivation warning: %s\n", audit.strDeriveError.c_str());
printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str());
printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n",
FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx);
printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n",
FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx);
printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str());
printf(" phrase-backed immature: %s BTF (%d transaction(s))\n",
FormatMoney(audit.nRecoverableImmatureCredit).c_str(),
audit.nRecoverableImmatureTx);
printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n",
FormatMoney(audit.nLegacyImmatureCredit).c_str(),
audit.nLegacyImmatureTx);

if (!audit.fHaveSeed)
{
printf("\n");
printf("This wallet has no recovery phrase. A file backup is the only backup.\n");
fflush(stdout);
return nTotal + nImmatureTotal > 0 ? 2 : 0;
}
if (!audit.fDeriveComplete)
{
printf("\n");
printf("Warning: the audit could not derive every known phrase key, so coverage is incomplete.\n");
fflush(stdout);
return 2;
}
if (audit.nLegacyCredit > 0 || audit.nLegacyImmatureCredit > 0)
{
printf("\n");
printf("Warning: some coins are on keys the phrase does not reproduce.\n");
printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n");
fflush(stdout);
return 2;
}

printf("\n");
if (nTotal + nImmatureTotal > 0)
printf("All known wallet balance is covered by the recovery phrase.\n");
else
printf("No wallet balance found yet.\n");
fflush(stdout);
return 0;
}

// Spend, from the command line.
//
// SendMoney() has been in this tree since 0.1.0 and only the window ever called
Expand Down
4 changes: 4 additions & 0 deletions src/walletcmd.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ int CmdNewAddress();
// Diagnostic: print the first nCount addresses this wallet's phrase derives.
int CmdShowDerived(int nCount);

// Diagnostic: print whether the currently spendable wallet balance is covered
// by the installed recovery phrase or still depends on wallet.dat-only keys.
int CmdRecoveryAudit();

#endif