From b65c19e559e2fec511f40a4d5481bcf6a35c311c Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 14:48:27 -0300 Subject: wallet: walk the chain for coins a key owns but the wallet never saw (#88) A transaction only entered the wallet when its block arrived over the network, so a key added afterwards was invisible: coins on the chain, private key in wallet.dat, balance zero. /importwallet has always ended that way -- it writes the keys and the money never shows up, which looks exactly like a failed import. ScanForWalletTransactions walks the main chain and hands every transaction that IsMine() to AddToWalletIfMine, the same two primitives the live path uses. Wired into /importwallet, and offered as /rescan. It is also the half that any recovery-phrase feature has to stand on: deriving keys from twelve words is easy, and without this a restore restores nothing. --- src/main.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.h | 5 +++++ src/main_gui.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index b027cef..68ae44c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -305,6 +305,55 @@ bool AddToWalletIfMine(const CTransaction& tx, const CBlock* pblock) return true; } +int ScanForWalletTransactions(CBlockIndex* pindexStart) +{ + // Walk the chain looking for transactions that belong to keys this wallet + // holds now, whatever it held when those blocks arrived. + // + // Nothing in this tree could do that. A key added after the fact -- from + // /importwallet, or from a backup restored onto a node that had since + // moved on -- was simply invisible: the coins were on the chain, the + // private key was in wallet.dat, and the balance did not show them, + // because a transaction only ever entered the wallet at the moment its + // block arrived over the network. + // + // Bitcoin grew the same function for the same reason. It is also what any + // recovery-phrase feature has to stand on: deriving the keys is the easy + // half, and without this the other half does not exist. + int nFound = 0; + int nScanned = 0; + CBlockIndex* pindex = pindexStart; + CRITICAL_BLOCK(cs_main) + { + while (pindex) + { + CBlock block; + if (block.ReadFromDisk(pindex, true)) + { + foreach(CTransaction& tx, block.vtx) + { + // Ask before handing it over: AddToWalletIfMine returns + // true for a transaction that is none of our business, so + // its return value cannot be counted. + if (tx.IsMine()) + { + if (AddToWalletIfMine(tx, &block)) + nFound++; + } + } + } + nScanned++; + if ((nScanned % 500) == 0) + printf("ScanForWalletTransactions() : %d blocks scanned, %d transaction(s) found\n", + nScanned, nFound); + pindex = pindex->pnext; + } + } + printf("ScanForWalletTransactions() : %d block(s) scanned, %d transaction(s) added or updated\n", + nScanned, nFound); + return nFound; +} + bool EraseFromWallet(uint256 hash) { CRITICAL_BLOCK(cs_mapWallet) diff --git a/src/main.h b/src/main.h index 1d9efac..8d9b7e6 100644 --- a/src/main.h +++ b/src/main.h @@ -152,6 +152,11 @@ void TopUpKeyPool(); vector GetKeyFromPool(); bool AddToWallet(const CWalletTx& wtxIn); void ReacceptWalletTransactions(); +// Walk the chain from pindexStart forward, adding every transaction that pays +// a key this wallet holds. Returns how many were added or updated. Needed by +// anything that puts a key into the wallet after the fact -- an import, a +// restored backup, and later a recovery phrase. +int ScanForWalletTransactions(CBlockIndex* pindexStart); // Returns how many wallet transactions the chain corrected. int RescanSpentFlags(); void RelayWalletTransactions(); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 5c87fe0..c9e27a9 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -76,6 +76,8 @@ static void PrintUsage() printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); printf(" anyone, so guard it like cash)\n"); printf(" /importwallet=FILE (load keys from such a file back in)\n"); + printf(" /rescan (walk the chain for coins this wallet owns\n"); + printf(" but never recorded, then exit)\n"); printf("\n"); printf("Each option also accepts '-' instead of '/'.\n"); } @@ -307,13 +309,37 @@ int main(int argc, char* argv[]) int nAdded = 0, nSkipped = 0; bool fOk = ImportWallet(strImport, nAdded, nSkipped); if (fOk) + { printf("Imported %d key(s); %d were already here.\n", nAdded, nSkipped); + // Importing a key without looking for its coins leaves the user + // holding a wallet that says zero about money that is on the + // chain. That was the old behaviour and it looks exactly like the + // import having failed. + if (nAdded > 0) + { + printf("Looking through the chain for coins belonging to the imported key(s)...\n"); + int nFound = ScanForWalletTransactions(pindexGenesisBlock); + printf("Found %d transaction(s). Start the node normally to see the balance.\n", nFound); + } + } else fprintf(stderr, "Import failed.\n"); DBFlush(true); return fOk ? 0 : 1; } + // /rescan -- walk the chain and pick up anything the wallet's keys own but + // the wallet never recorded. Cheap to offer and the only recourse when a + // balance is wrong for this reason. + if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) + { + printf("Rescanning the chain for this wallet's transactions...\n"); + int nFound = ScanForWalletTransactions(pindexGenesisBlock); + printf("Rescan done: %d transaction(s) added or updated.\n", nFound); + DBFlush(true); + return 0; + } + ReacceptWalletTransactions(); if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } -- cgit v1.2.3