diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-01 17:16:04 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-01 17:19:47 +0000 |
| commit | 1017644bc8d4bf183086ffb816c5e3f18068568e (patch) | |
| tree | 040e468018ad067811be744f55091294666f3fea | |
| parent | b4f0bc693ef7175702f82bcb2110b627922e8cbd (diff) | |
wallet: refuse to mint a wallet a headless node never asked for
v1.2.20 makes SQLite the default backend for a datadir that holds no
wallet. On a desktop that is a first run. On a headless node it is usually
a fault -- the datadir is not mounted, or wallet.dat was caught mid-rename
by a backup -- and the new default turns that fault into a silent loss of
custody rather than a loud failure.
The damage is not the empty wallet, it is the marker. Creating the wallet
also writes <datadir>/wallet-backend = sqlite, and every later start reads
that marker first: putting the real wallet.dat back no longer does
anything, because the node stays on the empty SQLite wallet it made.
1.2.19 had the same fail-open on a missing wallet, but it created a
wallet.dat, and restoring the backup over it recovered the funds. That
runbook stops working here.
So refuse when the startup is headless and no wallet of either kind is
present, and say which flag makes it deliberate: /createwallet to start a
new one, or /walletbackend=bdb for the Berkeley DB path. A node that meant
to create a wallet says so; a node that lost its datadir gets an error
instead of a fresh identity and a re-paid ledger.
Upstream keeps the old behaviour; this is a fork divergence, and the
three findings that would gate enabling the SQLite backend at all -- the
non-atomic backend marker, WriteHDMaster's connection-wide transaction,
and the encrypt-swap crash window -- remain open against upstream.
| -rw-r--r-- | src/main_gui.cpp | 29 | ||||
| -rw-r--r-- | src/selftest.cpp | 26 |
2 files changed, 50 insertions, 5 deletions
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 2743429..25cde1c 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -196,6 +196,9 @@ static void PrintUsage() | |||
| 196 | printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n"); | 196 | printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n"); |
| 197 | printf(" /walletbackend=sqlite (opt-in: run the node on <datadir>/wallet.sqlite\n"); | 197 | printf(" /walletbackend=sqlite (opt-in: run the node on <datadir>/wallet.sqlite\n"); |
| 198 | printf(" instead of wallet.dat; export one first)\n"); | 198 | printf(" instead of wallet.dat; export one first)\n"); |
| 199 | printf(" /createwallet (headless only: permit creating a new wallet in a\n"); | ||
| 200 | printf(" datadir that has none -- otherwise refused, since\n"); | ||
| 201 | printf(" a missing wallet is usually a mount/backup fault)\n"); | ||
| 199 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 202 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 200 | printf(" but never recorded, then exit)\n"); | 203 | printf(" but never recorded, then exit)\n"); |
| 201 | printf("\n"); | 204 | printf("\n"); |
| @@ -835,6 +838,32 @@ int main(int argc, char* argv[]) | |||
| 835 | else { fUseSQLite = true; fCreateSQLite = true; } | 838 | else { fUseSQLite = true; fCreateSQLite = true; } |
| 836 | } | 839 | } |
| 837 | 840 | ||
| 841 | // A headless node whose datadir holds no wallet of either kind is | ||
| 842 | // far more often an operations fault -- datadir not mounted, or | ||
| 843 | // wallet.dat caught mid-rename by a backup -- than a first run. | ||
| 844 | // Creating one here is not the harmless default it looks like: | ||
| 845 | // success also writes the backend marker, and from then on | ||
| 846 | // restoring the real wallet.dat no longer takes effect, because the | ||
| 847 | // marker keeps the node on the empty wallet it just made. Under | ||
| 848 | // 1.2.19 the same fault created a wallet.dat that a restore | ||
| 849 | // overwrote, so operators could recover by putting the file back. | ||
| 850 | // Refuse instead, and name the flag that makes it deliberate. | ||
| 851 | if (fCreateSQLite && fHeadlessStartup && | ||
| 852 | !arg(argc, argv, "/createwallet") && !arg(argc, argv, "-createwallet")) | ||
| 853 | { | ||
| 854 | AttachTerminal(); | ||
| 855 | fprintf(stderr, | ||
| 856 | "No wallet found in %s\n" | ||
| 857 | "Refusing to create one. On a headless node an absent wallet is\n" | ||
| 858 | "usually a mount or backup fault, and creating one would record a\n" | ||
| 859 | "backend marker that makes restoring the real wallet a no-op.\n" | ||
| 860 | "Pass -createwallet to start a new one deliberately, or\n" | ||
| 861 | "-walletbackend=bdb to run on a Berkeley DB wallet.dat.\n", | ||
| 862 | GetAppDir().c_str()); | ||
| 863 | fflush(stderr); | ||
| 864 | return 1; | ||
| 865 | } | ||
| 866 | |||
| 838 | if (fUseSQLite) | 867 | if (fUseSQLite) |
| 839 | { | 868 | { |
| 840 | AttachTerminal(); | 869 | AttachTerminal(); |
diff --git a/src/selftest.cpp b/src/selftest.cpp index 2ada8f3..c62d70f 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -2580,7 +2580,11 @@ static int RunWalletBackendDefaultSelfTest() | |||
| 2580 | 2580 | ||
| 2581 | try | 2581 | try |
| 2582 | { | 2582 | { |
| 2583 | // 1. Fresh datadir, no flag -> new wallet.sqlite, marker "sqlite", no wallet.dat. | 2583 | // 1. Fresh datadir, no flag -> this fork refuses, where upstream creates. |
| 2584 | // An empty datadir on a headless node is far more often a mount or | ||
| 2585 | // backup fault than a first run, and creating a wallet also pins the | ||
| 2586 | // backend marker, after which restoring the real wallet is a no-op. | ||
| 2587 | // Nothing may be left behind: no wallet of either kind, no marker. | ||
| 2584 | string d1 = tmp + "/fresh"; | 2588 | string d1 = tmp + "/fresh"; |
| 2585 | nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; | 2589 | nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; |
| 2586 | vector<string> a1; | 2590 | vector<string> a1; |
| @@ -2588,19 +2592,31 @@ static int RunWalletBackendDefaultSelfTest() | |||
| 2588 | a1.push_back("-nomanagedtor"); | 2592 | a1.push_back("-nomanagedtor"); |
| 2589 | a1.push_back("-nogui"); | 2593 | a1.push_back("-nogui"); |
| 2590 | a1.push_back("-newaddress"); | 2594 | a1.push_back("-newaddress"); |
| 2591 | int r1 = RunBitflashChild(strExe, a1); | 2595 | int r0 = RunBitflashChild(strExe, a1); |
| 2596 | nFail += Check(r0 != 0 && | ||
| 2597 | !FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2598 | !FileExists((d1 + "/wallet.dat").c_str()) && | ||
| 2599 | !FileExists((d1 + "/wallet-backend").c_str()), | ||
| 2600 | "a flagless headless start refuses to create a wallet") ? 0 : 1; | ||
| 2601 | |||
| 2602 | // 1a. -createwallet says it was meant: new wallet.sqlite, marker | ||
| 2603 | // "sqlite", no wallet.dat. | ||
| 2604 | vector<string> a1c = a1; | ||
| 2605 | a1c.push_back("-createwallet"); | ||
| 2606 | int r1 = RunBitflashChild(strExe, a1c); | ||
| 2592 | nFail += Check(r1 == 0 && | 2607 | nFail += Check(r1 == 0 && |
| 2593 | FileExists((d1 + "/wallet.sqlite").c_str()) && | 2608 | FileExists((d1 + "/wallet.sqlite").c_str()) && |
| 2594 | !FileExists((d1 + "/wallet.dat").c_str()) && | 2609 | !FileExists((d1 + "/wallet.dat").c_str()) && |
| 2595 | FileContainsText(d1 + "/wallet-backend", "sqlite"), | 2610 | FileContainsText(d1 + "/wallet-backend", "sqlite"), |
| 2596 | "a fresh install starts on SQLite (new default) and records the marker") ? 0 : 1; | 2611 | "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1; |
| 2597 | 2612 | ||
| 2598 | // 1b. Second run reuses it, still SQLite, still no wallet.dat. | 2613 | // 1b. Second run reuses it without the flag -- the refusal only guards |
| 2614 | // creation, so an existing wallet still starts flagless. | ||
| 2599 | int r1b = RunBitflashChild(strExe, a1); | 2615 | int r1b = RunBitflashChild(strExe, a1); |
| 2600 | nFail += Check(r1b == 0 && | 2616 | nFail += Check(r1b == 0 && |
| 2601 | FileExists((d1 + "/wallet.sqlite").c_str()) && | 2617 | FileExists((d1 + "/wallet.sqlite").c_str()) && |
| 2602 | !FileExists((d1 + "/wallet.dat").c_str()), | 2618 | !FileExists((d1 + "/wallet.dat").c_str()), |
| 2603 | "the recorded SQLite choice survives a restart") ? 0 : 1; | 2619 | "the recorded SQLite choice survives a flagless restart") ? 0 : 1; |
| 2604 | 2620 | ||
| 2605 | // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. | 2621 | // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. |
| 2606 | string d2 = tmp + "/existing-bdb"; | 2622 | string d2 = tmp + "/existing-bdb"; |