diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-04 05:59:40 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-04 05:59:40 +0000 |
| commit | 152c859b18b222a7c11a82b5485110e902267d40 (patch) | |
| tree | 1ceadf421609a2e58da81103165a2fb7085387de | |
| parent | e4f898c4a6b9900553c64b9804889b5fdae3e55f (diff) | |
| parent | b810d86752ddb2674118f737582c924bb9eb74c1 (diff) | |
Merge branch 'deploy/all-2026-08-24' into 'deploy/soak-2026-08-21'
deploy: land MRs 1-12 and upstream 1.2.20, as shipped in relay component
See merge request !13
| -rw-r--r-- | .github/workflows/ci.yml | 1 | ||||
| -rw-r--r-- | Makefile | 117 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | docs/storage.md | 159 | ||||
| -rwxr-xr-x | scripts/check-money-log-lines.sh | 38 | ||||
| -rw-r--r-- | src/Makefile | 15 | ||||
| -rw-r--r-- | src/btfrv.cpp | 65 | ||||
| -rw-r--r-- | src/btfrv.h | 19 | ||||
| -rw-r--r-- | src/btftunnel.cpp | 140 | ||||
| -rw-r--r-- | src/btftunnel.h | 25 | ||||
| -rw-r--r-- | src/db.cpp | 1251 | ||||
| -rw-r--r-- | src/db.h | 183 | ||||
| -rw-r--r-- | src/gui.cpp | 273 | ||||
| -rw-r--r-- | src/headers_core.h | 5 | ||||
| -rw-r--r-- | src/main.cpp | 578 | ||||
| -rw-r--r-- | src/main.h | 31 | ||||
| -rw-r--r-- | src/main_gui.cpp | 385 | ||||
| -rw-r--r-- | src/makefile.mingw | 12 | ||||
| -rw-r--r-- | src/net.cpp | 34 | ||||
| -rw-r--r-- | src/net.h | 90 | ||||
| -rw-r--r-- | src/nostr.cpp | 27 | ||||
| -rw-r--r-- | src/nostr.h | 3 | ||||
| -rw-r--r-- | src/randomx_pow.cpp | 28 | ||||
| -rw-r--r-- | src/readme.txt | 2 | ||||
| -rw-r--r-- | src/rpc.cpp | 852 | ||||
| -rw-r--r-- | src/selftest.cpp | 2083 | ||||
| -rw-r--r-- | src/selftest.h | 1 | ||||
| -rw-r--r-- | src/util.cpp | 153 | ||||
| -rw-r--r-- | src/util.h | 93 | ||||
| -rw-r--r-- | src/wallet_sqlite.cpp | 374 | ||||
| -rw-r--r-- | src/wallet_sqlite.h | 120 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 975 | ||||
| -rw-r--r-- | src/walletcmd.h | 34 |
33 files changed, 7836 insertions, 348 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4045f4..e22b5a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml | |||
| @@ -56,6 +56,7 @@ jobs: | |||
| 56 | mingw-w64-ucrt-x86_64-glfw | 56 | mingw-w64-ucrt-x86_64-glfw |
| 57 | mingw-w64-ucrt-x86_64-openssl | 57 | mingw-w64-ucrt-x86_64-openssl |
| 58 | mingw-w64-ucrt-x86_64-db | 58 | mingw-w64-ucrt-x86_64-db |
| 59 | mingw-w64-ucrt-x86_64-sqlite3 | ||
| 59 | mingw-w64-ucrt-x86_64-libsodium | 60 | mingw-w64-ucrt-x86_64-libsodium |
| 60 | mingw-w64-ucrt-x86_64-nlohmann-json | 61 | mingw-w64-ucrt-x86_64-nlohmann-json |
| 61 | mingw-w64-ucrt-x86_64-boost | 62 | mingw-w64-ucrt-x86_64-boost |
| @@ -11,10 +11,10 @@ | |||
| 11 | ROOT := $(shell pwd) | 11 | ROOT := $(shell pwd) |
| 12 | NPROC := $(shell nproc 2>/dev/null || echo 2) | 12 | NPROC := $(shell nproc 2>/dev/null || echo 2) |
| 13 | SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") | 13 | SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") |
| 14 | # Not "1.2.19". This tree is upstream v1.2.19 plus our own hardening commits and | 14 | # Not "1.2.20". This tree is upstream v1.2.20 plus our own hardening commits and |
| 15 | # a hand-merged payout ledger, so a binary built here must not be mistakable for | 15 | # a hand-merged payout ledger, so a binary built here must not be mistakable for |
| 16 | # the upstream release of that number -- it names every release artifact. | 16 | # the upstream release of that number -- it names every release artifact. |
| 17 | VERSION := 1.2.19+sec | 17 | VERSION := 1.2.20+sec |
| 18 | 18 | ||
| 19 | # ---- Pinned third-party sources ------------------------------------------- | 19 | # ---- Pinned third-party sources ------------------------------------------- |
| 20 | # | 20 | # |
| @@ -37,6 +37,12 @@ VERSION := 1.2.19+sec | |||
| 37 | # resolves the object by its own hash). Bump deliberately, and rebuild and | 37 | # resolves the object by its own hash). Bump deliberately, and rebuild and |
| 38 | # resync before shipping the result. | 38 | # resync before shipping the result. |
| 39 | # | 39 | # |
| 40 | # The check has to decide the exit status, so each recipe below joins its | ||
| 41 | # commands with && the whole way to the closing echo. Ending that chain with a | ||
| 42 | # `;` meant a failed `test` only skipped the build steps: the echo still ran, | ||
| 43 | # the target still exited 0, and make announced a pinned dependency that | ||
| 44 | # nothing had verified. | ||
| 45 | # | ||
| 40 | # RandomX pin is the commit this tree has been building and syncing against | 46 | # RandomX pin is the commit this tree has been building and syncing against |
| 41 | # (post-v2.0.1 master, 2026-05-24) -- identical to upstream's pin. | 47 | # (post-v2.0.1 master, 2026-05-24) -- identical to upstream's pin. |
| 42 | # | 48 | # |
| @@ -59,6 +65,32 @@ APPIMAGETOOL_SHA256 := ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3c | |||
| 59 | 65 | ||
| 60 | # ---- Linux ---------------------------------------------------------------- | 66 | # ---- Linux ---------------------------------------------------------------- |
| 61 | 67 | ||
| 68 | # RandomX used to build in $(HOME)/RandomX, and deps-randomx would rm -rf that | ||
| 69 | # path whenever the library was missing -- deleting a tree the build did not | ||
| 70 | # create and does not own (issue #27; a hand-managed pinned checkout there is | ||
| 71 | # what this machine's miners build against). The default is now in-tree, like | ||
| 72 | # secp256k1-build. A host that deliberately keeps RandomX elsewhere can set | ||
| 73 | # RANDOMX_DIR=... explicitly; the recipe below will still never delete a | ||
| 74 | # directory that lacks the marker file this build writes after a successful | ||
| 75 | # build. Exported so src/Makefile's own RANDOMX_DIR ?= picks the same tree. | ||
| 76 | # | ||
| 77 | # Every use of it below is quoted, and the recipe rejects a value that cannot | ||
| 78 | # name exactly one directory before it reaches anything destructive. Unquoted, | ||
| 79 | # the marker guard could be stepped around: a value holding a glob that matches | ||
| 80 | # two directories made `[ -d $(RANDOMX_DIR) ]` a two-argument test, which dies | ||
| 81 | # with "unexpected operator" instead of answering, so the && short-circuited, | ||
| 82 | # the guard was skipped, and the rm -rf below ran against every match. A value | ||
| 83 | # holding a space or a `&&` broke the outer test the same way and reported | ||
| 84 | # "already built". Whitespace and glob characters are refused outright rather | ||
| 85 | # than merely quoted, because src/Makefile interpolates this same value into | ||
| 86 | # -I$(RANDOMX_DIR)/src and -L$(RANDOMX_DIR)/build unquoted, where neither can | ||
| 87 | # survive. A relative value is refused rather than silently made absolute: it | ||
| 88 | # would mean two different directories to this Makefile and to src/Makefile, | ||
| 89 | # whose cwd is src/, and a command-line assignment reaches the sub-make through | ||
| 90 | # MAKEFLAGS, where it outranks anything exported from here. | ||
| 91 | RANDOMX_DIR ?= $(ROOT)/randomx-build | ||
| 92 | export RANDOMX_DIR | ||
| 93 | |||
| 62 | linux: deps-linux | 94 | linux: deps-linux |
| 63 | $(MAKE) -C src -f Makefile -j$(NPROC) | 95 | $(MAKE) -C src -f Makefile -j$(NPROC) |
| 64 | strip src/bitflash | 96 | strip src/bitflash |
| @@ -78,21 +110,35 @@ linux-node: deps-linux | |||
| 78 | 110 | ||
| 79 | deps-linux: deps-apt deps-secp256k1 deps-randomx | 111 | deps-linux: deps-apt deps-secp256k1 deps-randomx |
| 80 | 112 | ||
| 113 | # libboost-dev, not libboost-system-dev: Boost.System has been header-only | ||
| 114 | # since 1.69 and Ubuntu 26.04 (resolute) no longer ships the -system package. | ||
| 115 | # This tree only includes two boost headers and links no boost library. The | ||
| 116 | # apt-get update comes first: a fresh image's package lists are empty, and | ||
| 117 | # install then fails on the first name it cannot find. | ||
| 118 | # | ||
| 119 | # A failed update is deliberately not fatal: one unreachable repository should | ||
| 120 | # not block an install the lists already on disk can satisfy. It does say so, | ||
| 121 | # though -- "unable to locate package" several seconds later is otherwise hard | ||
| 122 | # to connect back to it. The install is the last command in the branch, so its | ||
| 123 | # status is the target's, and that one does abort. | ||
| 81 | deps-apt: | 124 | deps-apt: |
| 82 | @if command -v apt-get >/dev/null 2>&1; then \ | 125 | @if command -v apt-get >/dev/null 2>&1; then \ |
| 126 | $(SUDO) apt-get update || \ | ||
| 127 | echo " warning: apt-get update failed, using the package lists on disk"; \ | ||
| 83 | $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ | 128 | $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ |
| 84 | libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ | 129 | libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ |
| 85 | libboost-system-dev libglfw3-dev libgl-dev python3-pil clang; \ | 130 | libboost-dev libglfw3-dev libgl-dev libsqlite3-dev \ |
| 131 | python3-pil clang; \ | ||
| 86 | else \ | 132 | else \ |
| 87 | echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ | 133 | echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ |
| 88 | echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ | 134 | echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl sqlite3 python3-pil"; \ |
| 89 | fi | 135 | fi |
| 90 | 136 | ||
| 91 | deps-secp256k1: | 137 | deps-secp256k1: |
| 92 | @if [ ! -f /usr/local/lib/libsecp256k1.a ]; then \ | 138 | @if [ ! -f /usr/local/lib/libsecp256k1.a ]; then \ |
| 93 | echo "==> building libsecp256k1"; \ | 139 | echo "==> building libsecp256k1"; \ |
| 94 | rm -rf $(ROOT)/secp256k1-build; \ | 140 | rm -rf $(ROOT)/secp256k1-build && \ |
| 95 | git init -q $(ROOT)/secp256k1-build; \ | 141 | git init -q $(ROOT)/secp256k1-build && \ |
| 96 | cd $(ROOT)/secp256k1-build && \ | 142 | cd $(ROOT)/secp256k1-build && \ |
| 97 | git remote add origin $(SECP256K1_REPO) && \ | 143 | git remote add origin $(SECP256K1_REPO) && \ |
| 98 | git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ | 144 | git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ |
| @@ -102,26 +148,41 @@ deps-secp256k1: | |||
| 102 | cd $(ROOT)/secp256k1-build && ./autogen.sh && \ | 148 | cd $(ROOT)/secp256k1-build && ./autogen.sh && \ |
| 103 | ./configure --enable-module-schnorrsig --enable-module-extrakeys \ | 149 | ./configure --enable-module-schnorrsig --enable-module-extrakeys \ |
| 104 | --disable-shared --with-pic --disable-benchmark --disable-tests && \ | 150 | --disable-shared --with-pic --disable-benchmark --disable-tests && \ |
| 105 | make -j$(NPROC) && $(SUDO) make install && $(SUDO) ldconfig; \ | 151 | make -j$(NPROC) && $(SUDO) make install && $(SUDO) ldconfig && \ |
| 106 | echo "==> libsecp256k1 done"; \ | 152 | echo "==> libsecp256k1 done"; \ |
| 107 | else \ | 153 | else \ |
| 108 | echo "==> libsecp256k1 already installed"; \ | 154 | echo "==> libsecp256k1 already installed"; \ |
| 109 | fi | 155 | fi |
| 110 | 156 | ||
| 111 | deps-randomx: | 157 | deps-randomx: |
| 112 | @if [ ! -f $(HOME)/RandomX/build/librandomx.a ]; then \ | 158 | @ws="$$(printf ' \t')"; \ |
| 159 | case "$(RANDOMX_DIR)" in \ | ||
| 160 | "") echo "ERROR: RANDOMX_DIR is empty"; exit 1 ;; \ | ||
| 161 | /) echo "ERROR: RANDOMX_DIR is /, refusing"; exit 1 ;; \ | ||
| 162 | *[$$ws]*) echo "ERROR: RANDOMX_DIR contains whitespace: $(RANDOMX_DIR)"; exit 1 ;; \ | ||
| 163 | *[*?[]*) echo "ERROR: RANDOMX_DIR contains a glob character: $(RANDOMX_DIR)"; exit 1 ;; \ | ||
| 164 | /*) ;; \ | ||
| 165 | *) echo "ERROR: RANDOMX_DIR must be an absolute path: $(RANDOMX_DIR)"; exit 1 ;; \ | ||
| 166 | esac | ||
| 167 | @if [ ! -f "$(RANDOMX_DIR)/build/librandomx.a" ]; then \ | ||
| 168 | if [ -d "$(RANDOMX_DIR)" ] && [ ! -f "$(RANDOMX_DIR)/.bitflash-built" ]; then \ | ||
| 169 | echo "ERROR: $(RANDOMX_DIR) exists but was not created by this build"; \ | ||
| 170 | echo " refusing to rm -rf it -- remove it yourself or set RANDOMX_DIR"; \ | ||
| 171 | exit 1; \ | ||
| 172 | fi; \ | ||
| 113 | echo "==> building RandomX"; \ | 173 | echo "==> building RandomX"; \ |
| 114 | rm -rf $(HOME)/RandomX; \ | 174 | rm -rf "$(RANDOMX_DIR)" && \ |
| 115 | git init -q $(HOME)/RandomX; \ | 175 | git init -q "$(RANDOMX_DIR)" && \ |
| 116 | cd $(HOME)/RandomX && \ | 176 | cd "$(RANDOMX_DIR)" && \ |
| 117 | git remote add origin $(RANDOMX_REPO) && \ | 177 | git remote add origin $(RANDOMX_REPO) && \ |
| 118 | git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ | 178 | git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ |
| 119 | git checkout -q --detach FETCH_HEAD && \ | 179 | git checkout -q --detach FETCH_HEAD && \ |
| 120 | test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ | 180 | test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ |
| 121 | echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ | 181 | echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ |
| 122 | mkdir -p $(HOME)/RandomX/build; \ | 182 | mkdir -p "$(RANDOMX_DIR)/build" && \ |
| 123 | cd $(HOME)/RandomX/build && cmake .. -DCMAKE_BUILD_TYPE=Release && \ | 183 | cd "$(RANDOMX_DIR)/build" && cmake .. -DCMAKE_BUILD_TYPE=Release && \ |
| 124 | make -j$(NPROC) randomx; \ | 184 | make -j$(NPROC) randomx && \ |
| 185 | touch "$(RANDOMX_DIR)/.bitflash-built" && \ | ||
| 125 | echo "==> RandomX done"; \ | 186 | echo "==> RandomX done"; \ |
| 126 | else \ | 187 | else \ |
| 127 | echo "==> RandomX already built"; \ | 188 | echo "==> RandomX already built"; \ |
| @@ -172,13 +233,22 @@ img = ico.convert('RGBA'); \ | |||
| 172 | img.save(d + '/bitflash.png'); \ | 233 | img.save(d + '/bitflash.png'); \ |
| 173 | import shutil; shutil.copy(d + '/bitflash.png', \ | 234 | import shutil; shutil.copy(d + '/bitflash.png', \ |
| 174 | d + '/usr/share/icons/hicolor/256x256/apps/bitflash.png')" | 235 | d + '/usr/share/icons/hicolor/256x256/apps/bitflash.png')" |
| 175 | @for lib in libsodium.so.23 libdb_cxx-5.3.so libglfw.so.3; do \ | 236 | @for lib in libsodium.so.23 libdb_cxx-5.3.so libsqlite3.so.0 libglfw.so.3; do \ |
| 176 | path=$$(ldconfig -p | grep " $$lib " | awk '{print $$NF}' | head -1); \ | 237 | path=$$(ldconfig -p | grep " $$lib " | awk '{print $$NF}' | head -1); \ |
| 177 | [ -z "$$path" ] && path=$$(find /usr/lib /lib -name "$$lib" 2>/dev/null | head -1); \ | 238 | [ -z "$$path" ] && path=$$(find /usr/lib /lib -name "$$lib" 2>/dev/null | head -1); \ |
| 178 | [ -n "$$path" ] && cp -L "$$path" $(APPDIR)/usr/lib/ && echo " bundled $$lib" || echo " missing $$lib"; \ | 239 | [ -n "$$path" ] && cp -L "$$path" $(APPDIR)/usr/lib/ && echo " bundled $$lib" || echo " missing $$lib"; \ |
| 179 | done | 240 | done |
| 241 | # appimagetool's output used to be piped through grep ... || true, which | ||
| 242 | # discarded its exit status: the target succeeded and announced "Built:" | ||
| 243 | # even when no AppImage existed. Run the tool bare so its status decides, | ||
| 244 | # then require the artefact to exist, be non-empty, and be newer than the | ||
| 245 | # binary it was packaged from before saying so. | ||
| 180 | @ARCH=x86_64 $(APPIMAGETOOL) --no-appstream $(APPDIR) \ | 246 | @ARCH=x86_64 $(APPIMAGETOOL) --no-appstream $(APPDIR) \ |
| 181 | $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage 2>&1 | grep -E "^Built|^Error" || true | 247 | $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage |
| 248 | @test -s $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage || \ | ||
| 249 | { echo "ERROR: Bitflash-$(VERSION)-x86_64.AppImage missing or empty"; exit 1; } | ||
| 250 | @test $(ROOT)/Bitflash-$(VERSION)-x86_64.AppImage -nt src/bitflash || \ | ||
| 251 | { echo "ERROR: Bitflash-$(VERSION)-x86_64.AppImage is not newer than src/bitflash"; exit 1; } | ||
| 182 | @echo "Built: Bitflash-$(VERSION)-x86_64.AppImage" | 252 | @echo "Built: Bitflash-$(VERSION)-x86_64.AppImage" |
| 183 | 253 | ||
| 184 | # ---- Windows (MSYS2 UCRT64) ----------------------------------------------- | 254 | # ---- Windows (MSYS2 UCRT64) ----------------------------------------------- |
| @@ -190,14 +260,15 @@ deps-windows: | |||
| 190 | mingw-w64-ucrt-x86_64-glfw \ | 260 | mingw-w64-ucrt-x86_64-glfw \ |
| 191 | mingw-w64-ucrt-x86_64-openssl \ | 261 | mingw-w64-ucrt-x86_64-openssl \ |
| 192 | mingw-w64-ucrt-x86_64-db \ | 262 | mingw-w64-ucrt-x86_64-db \ |
| 263 | mingw-w64-ucrt-x86_64-sqlite3 \ | ||
| 193 | mingw-w64-ucrt-x86_64-libsodium \ | 264 | mingw-w64-ucrt-x86_64-libsodium \ |
| 194 | mingw-w64-ucrt-x86_64-nlohmann-json \ | 265 | mingw-w64-ucrt-x86_64-nlohmann-json \ |
| 195 | mingw-w64-ucrt-x86_64-boost \ | 266 | mingw-w64-ucrt-x86_64-boost \ |
| 196 | mingw-w64-ucrt-x86_64-cmake \ | 267 | mingw-w64-ucrt-x86_64-cmake \ |
| 197 | autoconf automake libtool | 268 | autoconf automake libtool |
| 198 | @if [ ! -f deps/lib/libsecp256k1.a ]; then \ | 269 | @if [ ! -f deps/lib/libsecp256k1.a ]; then \ |
| 199 | rm -rf secp256k1-build; \ | 270 | rm -rf secp256k1-build && \ |
| 200 | git init -q secp256k1-build; \ | 271 | git init -q secp256k1-build && \ |
| 201 | cd secp256k1-build && \ | 272 | cd secp256k1-build && \ |
| 202 | git remote add origin $(SECP256K1_REPO) && \ | 273 | git remote add origin $(SECP256K1_REPO) && \ |
| 203 | git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ | 274 | git fetch -q --depth 1 origin $(SECP256K1_COMMIT) && \ |
| @@ -208,23 +279,23 @@ deps-windows: | |||
| 208 | ./configure --prefix=$(ROOT)/deps \ | 279 | ./configure --prefix=$(ROOT)/deps \ |
| 209 | --enable-module-schnorrsig --enable-module-extrakeys \ | 280 | --enable-module-schnorrsig --enable-module-extrakeys \ |
| 210 | --disable-shared --with-pic --disable-benchmark --disable-tests && \ | 281 | --disable-shared --with-pic --disable-benchmark --disable-tests && \ |
| 211 | make install; \ | 282 | make install && \ |
| 212 | echo "==> secp256k1 done"; \ | 283 | echo "==> secp256k1 done"; \ |
| 213 | else \ | 284 | else \ |
| 214 | echo "==> secp256k1 already built"; \ | 285 | echo "==> secp256k1 already built"; \ |
| 215 | fi | 286 | fi |
| 216 | @if [ ! -f RandomX/build/librandomx.a ]; then \ | 287 | @if [ ! -f RandomX/build/librandomx.a ]; then \ |
| 217 | rm -rf RandomX; \ | 288 | rm -rf RandomX && \ |
| 218 | git init -q RandomX; \ | 289 | git init -q RandomX && \ |
| 219 | cd RandomX && \ | 290 | cd RandomX && \ |
| 220 | git remote add origin $(RANDOMX_REPO) && \ | 291 | git remote add origin $(RANDOMX_REPO) && \ |
| 221 | git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ | 292 | git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ |
| 222 | git checkout -q --detach FETCH_HEAD && \ | 293 | git checkout -q --detach FETCH_HEAD && \ |
| 223 | test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ | 294 | test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ |
| 224 | echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ | 295 | echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ |
| 225 | mkdir -p $(ROOT)/RandomX/build; \ | 296 | mkdir -p $(ROOT)/RandomX/build && \ |
| 226 | cd $(ROOT)/RandomX/build && cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release && \ | 297 | cd $(ROOT)/RandomX/build && cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release && \ |
| 227 | make randomx; \ | 298 | make randomx && \ |
| 228 | echo "==> RandomX done"; \ | 299 | echo "==> RandomX done"; \ |
| 229 | else \ | 300 | else \ |
| 230 | echo "==> RandomX already built"; \ | 301 | echo "==> RandomX already built"; \ |
| @@ -289,12 +289,22 @@ See [public pool directory](docs/pool-directory.md) for the website/API shape. | |||
| 289 | **Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. | 289 | **Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. |
| 290 | 290 | ||
| 291 | The built-in participant miner connects to operator pools through the `.btf` | 291 | The built-in participant miner connects to operator pools through the `.btf` |
| 292 | path. External miners can use Bitflash as a local Stratum bridge: | 292 | path. Use it — **third-party RandomX miners such as XMRig and SRBMiner cannot |
| 293 | mine Bitflash pools**, and no amount of configuration changes that. In RandomX | ||
| 294 | mode they speak the Monero-style JSON-RPC protocol (`login`/`job`/`submit`) and | ||
| 295 | assume a Monero-style job: the nonce at byte 39 of the blob, and a 32-byte seed | ||
| 296 | hash given as 64 hex characters. Bitflash pools speak Bitcoin-style Stratum | ||
| 297 | (`mining.subscribe`/`mining.notify`/`mining.submit`) over an 80-byte header | ||
| 298 | whose nonce is at byte 76 — byte 39 falls inside the merkle root — and key | ||
| 299 | RandomX from a fixed 36-byte string that is not a hash at all. | ||
| 300 | |||
| 301 | `-stratumbridge` carries that same Bitcoin-style Stratum over the `.btf` path, | ||
| 302 | so a miner on this machine can reach a pool it otherwise could not address. It | ||
| 303 | is a byte relay, not a protocol translator, so it helps only a miner that | ||
| 304 | already speaks the pool's dialect, and it does not make the miners above work: | ||
| 293 | 305 | ||
| 294 | ```bash | 306 | ```bash |
| 295 | ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS -stratumbridgeport=3333 | 307 | ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS -stratumbridgeport=3333 |
| 296 | SRBMiner-MULTI --algorithm randomx --pool 127.0.0.1:3333 --wallet YOUR_BTF_ADDRESS --password x | ||
| 297 | xmrig -a rx/0 -o 127.0.0.1:3333 -u YOUR_BTF_ADDRESS -p x | ||
| 298 | ``` | 308 | ``` |
| 299 | 309 | ||
| 300 | Estimate rewards and electricity cost locally: | 310 | Estimate rewards and electricity cost locally: |
| @@ -322,7 +332,7 @@ Bitflash has a reliable public market. | |||
| 322 | -poolstatusfile=/var/www/pool_status.json \ | 332 | -poolstatusfile=/var/www/pool_status.json \ |
| 323 | -poolroundsfile=/var/www/pool_rounds.json | 333 | -poolroundsfile=/var/www/pool_rounds.json |
| 324 | ./bitflash -nogui -gen -participant=POOL_BTF_ADDRESS # mine to pool | 334 | ./bitflash -nogui -gen -participant=POOL_BTF_ADDRESS # mine to pool |
| 325 | ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS # local bridge for XMRig/SRBMiner | 335 | ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS # Stratum over .btf (see Mining) |
| 326 | ``` | 336 | ``` |
| 327 | 337 | ||
| 328 | Every option takes `-` or `/`. **Under MSYS2 use the `-` form** — the shell | 338 | Every option takes `-` or `/`. **Under MSYS2 use the `-` form** — the shell |
diff --git a/docs/storage.md b/docs/storage.md new file mode 100644 index 0000000..c7de4e7 --- /dev/null +++ b/docs/storage.md | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | # Bitflash Storage Inventory | ||
| 2 | |||
| 3 | This document records the Berkeley DB surface that still exists in Bitflash. | ||
| 4 | It is a migration map, not a promise that all stores should move at once. | ||
| 5 | |||
| 6 | The immediate priority is `wallet.dat`: it holds spend authority and recovery | ||
| 7 | metadata. `blkindex.dat` can be rebuilt from block files, so it is important for | ||
| 8 | startup correctness but less dangerous than wallet storage. | ||
| 9 | |||
| 10 | ## Berkeley DB Environment | ||
| 11 | |||
| 12 | All `CDB` stores use one Berkeley DB environment rooted at: | ||
| 13 | |||
| 14 | ```text | ||
| 15 | <datadir>/database/ | ||
| 16 | ``` | ||
| 17 | |||
| 18 | The environment creates log files such as: | ||
| 19 | |||
| 20 | ```text | ||
| 21 | database/log.0000000001 | ||
| 22 | ``` | ||
| 23 | |||
| 24 | Those logs are part of the legacy BDB risk model. They are not user-facing | ||
| 25 | wallet files, but they can contain historical records while the environment is | ||
| 26 | live. Wallet encryption already rewrites `wallet.dat` and purges BDB environment | ||
| 27 | logs after the encrypted file is installed. | ||
| 28 | |||
| 29 | ## Common Records | ||
| 30 | |||
| 31 | Every `CDB` file can carry: | ||
| 32 | |||
| 33 | | Key | Value | Purpose | | ||
| 34 | |---|---|---| | ||
| 35 | | `"version"` | `int` | Database format version written when a DB is opened in create mode. | | ||
| 36 | |||
| 37 | Keys and values are serialized with the existing `CDataStream` format. A future | ||
| 38 | SQLite migration must preserve byte-for-byte value semantics until a higher | ||
| 39 | level schema migration is intentionally introduced. | ||
| 40 | |||
| 41 | ## `wallet.dat` | ||
| 42 | |||
| 43 | `wallet.dat` is accessed through `CWalletDB`. | ||
| 44 | Read-only tooling should go through `ScanWalletRecords()` instead of opening a | ||
| 45 | Berkeley DB cursor directly. That keeps migration checks, audits, and future | ||
| 46 | backends behind one record-stream boundary. Wallet rewrite code should also use | ||
| 47 | that boundary when it needs to copy existing records into a newly written | ||
| 48 | wallet file. | ||
| 49 | |||
| 50 | | Key shape | Value | Secret? | Meaning | | ||
| 51 | |---|---|---:|---| | ||
| 52 | | `("name", address)` | `string` | No | Address book label. | | ||
| 53 | | `("tx", txid)` | `CWalletTx` | No | Wallet transaction state, including spend flags and metadata. | | ||
| 54 | | `("key", pubkey)` | `CPrivKey` | Yes | Plain private key record for unencrypted wallets or old backups. | | ||
| 55 | | `"defaultkey"` | `vector<unsigned char>` | No | Default public key shown by the wallet. | | ||
| 56 | | `"hdmaster"` | `vector<unsigned char>` | Yes | Plain BIP32 master private key material. | | ||
| 57 | | `"hdchaincode"` | `vector<unsigned char>` | Yes | Plain BIP32 chain code. | | ||
| 58 | | `"hdnext"` | `unsigned int` | No | Legacy deterministic derivation next index. | | ||
| 59 | | `"hdschema"` | `int` | No | Deterministic derivation schema (`legacy` or `bip44`). | | ||
| 60 | | `"hdcointype"` | `unsigned int` | No | BIP44 coin type. Bitflash is `4346950`. | | ||
| 61 | | `"hdreceivenext"` | `unsigned int` | No | BIP44 external receive-chain next index. | | ||
| 62 | | `"hdchangenext"` | `unsigned int` | No | BIP44 internal change-chain next index. | | ||
| 63 | | `("mkey", id)` | `CWalletMasterKey` | Sensitive | Encrypted wallet master-key metadata. | | ||
| 64 | | `("ckey", pubkey)` | `vector<unsigned char>` | Yes | Private key encrypted under the wallet master key. | | ||
| 65 | | `"cryptedhdmaster"` | `vector<unsigned char>` | Yes | Encrypted BIP32 master private key material. | | ||
| 66 | | `"cryptedhdchaincode"` | `vector<unsigned char>` | Yes | Encrypted BIP32 chain code. | | ||
| 67 | | `"walletminversion"` | `int` | No | Minimum wallet format version required to open this wallet safely. | | ||
| 68 | | `("pool", index)` | `vector<unsigned char>` | No | Public key reserved in the key pool. The private key lives in `key`/`ckey`. | | ||
| 69 | | `("setting", name)` | typed setting value | Usually no | Wallet/UI settings such as mining mode, fees, and backup timestamps. | | ||
| 70 | |||
| 71 | ### Wallet Migration Rules | ||
| 72 | |||
| 73 | A future wallet backend must keep these properties: | ||
| 74 | |||
| 75 | 1. Unknown future wallet formats fail closed with a clear message. | ||
| 76 | 2. Encrypted wallets must encrypt private keys and HD seed material. | ||
| 77 | 3. A locked encrypted wallet may expose public keys and balances, but not | ||
| 78 | private keys or decrypted HD seed material. | ||
| 79 | 4. `-backupwallet` must produce a wallet file that opens in a different datadir | ||
| 80 | without copying `database/`, and must not leave a half-written final backup | ||
| 81 | path if the copy or install step fails. | ||
| 82 | 5. `-recoveryaudit` must distinguish: | ||
| 83 | - no recovery phrase installed; | ||
| 84 | - recovery phrase present; | ||
| 85 | - recovery phrase encrypted and locked, requiring unlock before coverage can | ||
| 86 | be audited. | ||
| 87 | |||
| 88 | ## SQLite Wallet Store | ||
| 89 | |||
| 90 | Since v1.2.20 SQLite is the default wallet backend. It is a byte-preserving | ||
| 91 | record store, not a new wallet format -- the same serialized `CDataStream` bytes | ||
| 92 | `wallet.dat` uses: | ||
| 93 | |||
| 94 | ```sql | ||
| 95 | CREATE TABLE wallet_records ( | ||
| 96 | key BLOB PRIMARY KEY NOT NULL, | ||
| 97 | value BLOB NOT NULL | ||
| 98 | ); | ||
| 99 | PRAGMA user_version=1; | ||
| 100 | ``` | ||
| 101 | |||
| 102 | A fresh data directory starts on `wallet.sqlite`. An existing `wallet.dat` keeps | ||
| 103 | running on Berkeley DB until it is converted: the GUI offers a one-click | ||
| 104 | conversion on first run, and `-walletbackend=sqlite` / `-walletbackend=bdb` | ||
| 105 | (recorded in a plain-text `wallet-backend` marker file in the datadir) select a | ||
| 106 | backend explicitly. Conversion never modifies `wallet.dat`; it stays as a | ||
| 107 | fallback, and the change takes effect on restart. | ||
| 108 | |||
| 109 | Every wallet operation runs on either backend: read, write, erase, key-pool and | ||
| 110 | HD-seed writes (the two halves of the seed bracketed in one SQLite transaction), | ||
| 111 | `-backupwallet` (checkpoint then copy the self-contained file), `-encryptwallet` | ||
| 112 | (an atomic rebuild-and-swap in one transaction), and the `-walletstorageaudit` / | ||
| 113 | `-walletstoragecheck` diagnostics, which read the active backend's file. | ||
| 114 | |||
| 115 | The migration tooling remains available for scripted use. `-walletsqliteexport=FILE` | ||
| 116 | streams every raw `wallet.dat` record through `ScanWalletRecords()` into a SQLite | ||
| 117 | file in one transaction, refusing to overwrite. `-walletsqliteverify=FILE` | ||
| 118 | compares the two streams and prints only counts. `-walletsqliteloadcheck=FILE` | ||
| 119 | parses an export with the same record types the loader uses. | ||
| 120 | `-walletsqliterestore=FILE` rebuilds a `wallet.dat` from an export in an empty | ||
| 121 | data directory. Failed exports and restores remove their incomplete output. | ||
| 122 | |||
| 123 | ## `blkindex.dat` | ||
| 124 | |||
| 125 | `blkindex.dat` is accessed through `CTxDB`. | ||
| 126 | |||
| 127 | | Key shape | Value | Rebuildable? | Meaning | | ||
| 128 | |---|---|---:|---| | ||
| 129 | | `("tx", txid)` | `CTxIndex` | Yes | Transaction disk position and spent-output index state. | | ||
| 130 | | `("owner", hash160, CDiskTxPos)` | `int height` | Yes | Legacy owner transaction index cursor range. | | ||
| 131 | | `("blockindex", blockhash)` | `CDiskBlockIndex` | Yes | Block index entry. | | ||
| 132 | | `"hashBestChain"` | `uint256` | Yes | Current best-chain tip hash. | | ||
| 133 | |||
| 134 | `blkindex.dat` can be rebuilt from `blk*.dat` plus consensus validation. A | ||
| 135 | future migration may therefore prefer a rebuild path over a byte-preserving | ||
| 136 | conversion path. | ||
| 137 | |||
| 138 | ## Other Legacy Stores | ||
| 139 | |||
| 140 | None. Upstream still documents `reviews.dat` (`CReviewDB`) and `market.dat` | ||
| 141 | (`CMarketDB`) here; this fork deleted the 2009 marketplace outright, along | ||
| 142 | with `CReviewDB`, `market.cpp` and `market.h`, so neither file is opened or | ||
| 143 | created by this tree. A datadir carrying them from an older build is simply | ||
| 144 | ignored. | ||
| 145 | |||
| 146 | The old IP address database (`addr.dat`) has been removed. Peer discovery is | ||
| 147 | handled by Nostr, `.btf`, rendezvous relays, and direct onion peers. | ||
| 148 | |||
| 149 | ## Migration Order (shipped in v1.2.20) | ||
| 150 | |||
| 151 | 1. Storage self-tests were added around Berkeley DB behavior first. | ||
| 152 | 2. The SQLite backend landed behind an explicit flag, then behind a | ||
| 153 | `wallet-backend` marker and a GUI conversion wizard. | ||
| 154 | 3. BDB-to-SQLite migration is a one-way copy that leaves `wallet.dat` as a backup. | ||
| 155 | 4. SQLite became the default for new wallets only after cross-version restore, | ||
| 156 | encryption, backup, and recovery-audit tests passed -- and a live coin-proof | ||
| 157 | of receive, spend, restart, and encrypt on real mainnet funds. | ||
| 158 | 5. Treat `blkindex.dat` separately; it is safer to rebuild than to migrate under | ||
| 159 | pressure. | ||
diff --git a/scripts/check-money-log-lines.sh b/scripts/check-money-log-lines.sh new file mode 100755 index 0000000..c9f348b --- /dev/null +++ b/scripts/check-money-log-lines.sh | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # Money-moving payout lines must print every time they fire: they are the | ||
| 3 | # audit trail for obligations created, moved and destroyed, and LogPrint's | ||
| 4 | # burst collapse can stand one line in for any number of events inside one | ||
| 5 | # 3-second window (issue #3: 100 voids totalling 4,950 BTF read as one | ||
| 6 | # 49.49 BTF line). Every line carrying one of the phrases below must use | ||
| 7 | # LogPrintAlways, which keeps the category gate but skips the collapse. | ||
| 8 | # | ||
| 9 | # Adding a payout line that reports money moving? Use LogPrintAlways and | ||
| 10 | # add its phrase here. A phrase found in a plain LogPrint call fails this | ||
| 11 | # check; anything else passes, so a non-money line is never held up by it. | ||
| 12 | set -euo pipefail | ||
| 13 | cd "$(dirname "$0")/.." | ||
| 14 | |||
| 15 | phrases='was orphaned -- voiding|orphaned while queued|\[payout\] paid %s to %s|FAILED to pay|writing off|SKIPPED bad address' | ||
| 16 | |||
| 17 | # Call sites span several lines; flatten each call to a single line first so | ||
| 18 | # the phrase and the macro it belongs to are matched together. Only a line | ||
| 19 | # whose first token is the macro starts a call, so comments mentioning | ||
| 20 | # LogPrint are never mistaken for one. | ||
| 21 | bad=$(awk ' | ||
| 22 | /^[ \t]*LogPrint(Always)?[(]/ { | ||
| 23 | if (collecting) print buf | ||
| 24 | buf = $0; collecting = 1; next | ||
| 25 | } | ||
| 26 | collecting { | ||
| 27 | buf = buf $0 | ||
| 28 | if (/;[ \t]*$/) { print buf; collecting = 0 } | ||
| 29 | } | ||
| 30 | END { if (collecting) print buf } | ||
| 31 | ' src/rpc.cpp | grep -E '^[[:space:]]*LogPrint[(]' | grep -E "$phrases" || true) | ||
| 32 | |||
| 33 | if [ -n "$bad" ]; then | ||
| 34 | echo "error: money-moving payout line(s) on collapsing LogPrint:" >&2 | ||
| 35 | echo "$bad" >&2 | ||
| 36 | exit 1 | ||
| 37 | fi | ||
| 38 | echo "ok: every money-moving payout line in src/rpc.cpp uses LogPrintAlways" | ||
diff --git a/src/Makefile b/src/Makefile index 1bc3ae8..b931007 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -6,7 +6,9 @@ FUZZ_CXX ?= clang++ | |||
| 6 | 6 | ||
| 7 | SECP_INC ?= /usr/local/include | 7 | SECP_INC ?= /usr/local/include |
| 8 | SECP_LIB ?= /usr/local/lib | 8 | SECP_LIB ?= /usr/local/lib |
| 9 | RANDOMX_DIR ?= $(HOME)/RandomX | 9 | # The root Makefile exports RANDOMX_DIR (default: <repo>/randomx-build, built |
| 10 | # by deps-randomx). This fallback only applies when src/ is built standalone. | ||
| 11 | RANDOMX_DIR ?= $(abspath $(CURDIR)/..)/randomx-build | ||
| 10 | 12 | ||
| 11 | IMGUI_DIR = imgui | 13 | IMGUI_DIR = imgui |
| 12 | 14 | ||
| @@ -40,7 +42,7 @@ FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined | |||
| 40 | 42 | ||
| 41 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 43 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ |
| 42 | -L$(RANDOMX_DIR)/build -lrandomx \ | 44 | -L$(RANDOMX_DIR)/build -lrandomx \ |
| 43 | -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ | 45 | -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ |
| 44 | -lglfw -lGL | 46 | -lglfw -lGL |
| 45 | 47 | ||
| 46 | # Every header a .cpp here includes has to be listed, not just the interesting | 48 | # Every header a .cpp here includes has to be listed, not just the interesting |
| @@ -53,7 +55,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ | |||
| 53 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 55 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 54 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ | 56 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ |
| 55 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ | 57 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ |
| 56 | selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h | 58 | selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h |
| 57 | 59 | ||
| 58 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 60 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ |
| 59 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o | 61 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o |
| @@ -61,7 +63,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | |||
| 61 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 63 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 62 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 64 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 63 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ | 65 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 64 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o | 66 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ |
| 67 | obj/proxy.o obj/tor.o | ||
| 65 | 68 | ||
| 66 | GUI_OBJS = obj/gui.o obj/main_gui.o | 69 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 67 | 70 | ||
| @@ -130,7 +133,7 @@ LDHARDFLAGS = -Wl,-z,noexecstack | |||
| 130 | 133 | ||
| 131 | NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 134 | NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ |
| 132 | -L$(RANDOMX_DIR)/build -lrandomx \ | 135 | -L$(RANDOMX_DIR)/build -lrandomx \ |
| 133 | -lsodium -lssl -lcrypto -ldb_cxx -lpthread | 136 | -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread |
| 134 | 137 | ||
| 135 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ | 138 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ |
| 136 | obj-node/main_gui.o obj-node/headless.o | 139 | obj-node/main_gui.o obj-node/headless.o |
| @@ -201,6 +204,8 @@ tests: test_bip32 bitflash-node | |||
| 201 | ./bitflash-node -selftest=wallet-keypool | 204 | ./bitflash-node -selftest=wallet-keypool |
| 202 | ./bitflash-node -selftest=wallet-hd | 205 | ./bitflash-node -selftest=wallet-hd |
| 203 | ./bitflash-node -selftest=wallet-format | 206 | ./bitflash-node -selftest=wallet-format |
| 207 | ./bitflash-node -selftest=wallet-sqlite | ||
| 208 | ./bitflash-node -selftest=wallet-sqlite-migration | ||
| 204 | ./bitflash-node -selftest=wallet-crypto | 209 | ./bitflash-node -selftest=wallet-crypto |
| 205 | ./bitflash-node -selftest=wallet-encrypt | 210 | ./bitflash-node -selftest=wallet-encrypt |
| 206 | ./bitflash-node -selftest=net-message | 211 | ./bitflash-node -selftest=net-message |
diff --git a/src/btfrv.cpp b/src/btfrv.cpp index 133eb89..7d705ef 100644 --- a/src/btfrv.cpp +++ b/src/btfrv.cpp | |||
| @@ -40,6 +40,8 @@ typedef int SOCKET; | |||
| 40 | #include <thread> | 40 | #include <thread> |
| 41 | #include <mutex> | 41 | #include <mutex> |
| 42 | #include <atomic> | 42 | #include <atomic> |
| 43 | #include <chrono> | ||
| 44 | #include <system_error> | ||
| 43 | 45 | ||
| 44 | namespace btf | 46 | namespace btf |
| 45 | { | 47 | { |
| @@ -209,8 +211,28 @@ static void HandleIncoming(SOCKET s) | |||
| 209 | bool a = WriteN(svc, &ok, 1); | 211 | bool a = WriteN(svc, &ok, 1); |
| 210 | bool b = WriteN(s, &ok, 1); | 212 | bool b = WriteN(s, &ok, 1); |
| 211 | if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; } | 213 | if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; } |
| 212 | std::thread(Forward, s, svc).detach(); | 214 | // One thread pumps s -> svc; this thread pumps svc -> s. Spawning a |
| 213 | std::thread(Forward, svc, s).detach(); | 215 | // thread per direction left no clean way to fail the second spawn: |
| 216 | // std::thread throws std::system_error when pthread_create is refused | ||
| 217 | // (NPROC limit, container pids cgroup, memory pressure), the first | ||
| 218 | // pump was already detached, and closing its sockets from here would | ||
| 219 | // race it -- so nothing caught the throw and it reached | ||
| 220 | // std::terminate, killing the relay over one pair it could not serve. | ||
| 221 | // Running one direction on this thread leaves exactly one spawn to | ||
| 222 | // guard, and a failure here has nothing running yet to race. | ||
| 223 | try | ||
| 224 | { | ||
| 225 | std::thread(Forward, s, svc).detach(); | ||
| 226 | } | ||
| 227 | catch (const std::system_error& e) | ||
| 228 | { | ||
| 229 | fprintf(stderr, "rendezvous relay: cannot spawn forward thread (%s), pair dropped\n", | ||
| 230 | e.what()); | ||
| 231 | CLOSESOCK(svc); | ||
| 232 | CLOSESOCK(s); | ||
| 233 | return; | ||
| 234 | } | ||
| 235 | Forward(svc, s); | ||
| 214 | } | 236 | } |
| 215 | else | 237 | else |
| 216 | { | 238 | { |
| @@ -236,6 +258,7 @@ bool RvRelayRun(unsigned short port) | |||
| 236 | if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } | 258 | if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } |
| 237 | if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } | 259 | if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } |
| 238 | 260 | ||
| 261 | int nSpawnFails = 0; | ||
| 239 | while (!g_stop) | 262 | while (!g_stop) |
| 240 | { | 263 | { |
| 241 | struct sockaddr_in cli; | 264 | struct sockaddr_in cli; |
| @@ -246,7 +269,27 @@ bool RvRelayRun(unsigned short port) | |||
| 246 | if (g_stop) break; | 269 | if (g_stop) break; |
| 247 | continue; | 270 | continue; |
| 248 | } | 271 | } |
| 249 | std::thread(HandleIncoming, s).detach(); | 272 | try |
| 273 | { | ||
| 274 | std::thread(HandleIncoming, s).detach(); | ||
| 275 | nSpawnFails = 0; | ||
| 276 | } | ||
| 277 | catch (const std::system_error& e) | ||
| 278 | { | ||
| 279 | // A refused pthread_create is backpressure, not a reason to die: | ||
| 280 | // the condition behind it (NPROC, pids cgroup, memory) does not | ||
| 281 | // clear in milliseconds, and the connection count feeding it is | ||
| 282 | // attacker-influenced. Drop this one peer, stop accepting for a | ||
| 283 | // moment -- longer each time, capped -- and keep serving the | ||
| 284 | // connections already established. | ||
| 285 | fprintf(stderr, "rendezvous relay: cannot spawn connection thread (%s), connection dropped\n", | ||
| 286 | e.what()); | ||
| 287 | CLOSESOCK(s); | ||
| 288 | nSpawnFails++; | ||
| 289 | int nWaitMs = nSpawnFails < 5 ? nSpawnFails * 1000 : 5000; | ||
| 290 | for (int i = 0; i < nWaitMs && !g_stop; i += 100) | ||
| 291 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
| 292 | } | ||
| 250 | } | 293 | } |
| 251 | CLOSESOCK(g_listen); | 294 | CLOSESOCK(g_listen); |
| 252 | g_listen = INVALID_SOCKET; | 295 | g_listen = INVALID_SOCKET; |
| @@ -334,17 +377,29 @@ bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs) | |||
| 334 | } | 377 | } |
| 335 | 378 | ||
| 336 | RvSocket RvClientConnect(const char* relay_host, unsigned short port, | 379 | RvSocket RvClientConnect(const char* relay_host, unsigned short port, |
| 337 | const unsigned char target_pubkey[32]) | 380 | const unsigned char target_pubkey[32], |
| 381 | RvClientFail* pFail) | ||
| 338 | { | 382 | { |
| 383 | if (pFail) *pFail = RVC_FAIL_DIAL; | ||
| 339 | if (!RvInit()) return RV_INVALID; | 384 | if (!RvInit()) return RV_INVALID; |
| 340 | SOCKET s = ConnectTo(relay_host, port); | 385 | SOCKET s = ConnectTo(relay_host, port); |
| 341 | if (s == INVALID_SOCKET) return RV_INVALID; | 386 | if (s == INVALID_SOCKET) return RV_INVALID; |
| 387 | if (pFail) *pFail = RVC_FAIL_PAIR; | ||
| 342 | unsigned char hdr[33]; | 388 | unsigned char hdr[33]; |
| 343 | hdr[0] = (unsigned char)'C'; | 389 | hdr[0] = (unsigned char)'C'; |
| 344 | memcpy(hdr + 1, target_pubkey, 32); | 390 | memcpy(hdr + 1, target_pubkey, 32); |
| 345 | if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; } | 391 | if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; } |
| 346 | unsigned char paired = 0; | 392 | unsigned char paired = 0; |
| 347 | if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; } | 393 | if (!ReadN(s, &paired, 1)) { CLOSESOCK(s); return RV_INVALID; } |
| 394 | if (paired != 0x01) | ||
| 395 | { | ||
| 396 | // The relay answered and declined: HandleIncoming writes 0x00 and | ||
| 397 | // closes when no service is parked under this pubkey right now. | ||
| 398 | if (pFail) *pFail = RVC_FAIL_REFUSED; | ||
| 399 | CLOSESOCK(s); | ||
| 400 | return RV_INVALID; | ||
| 401 | } | ||
| 402 | if (pFail) *pFail = RVC_FAIL_NONE; | ||
| 348 | return (RvSocket)s; | 403 | return (RvSocket)s; |
| 349 | } | 404 | } |
| 350 | 405 | ||
diff --git a/src/btfrv.h b/src/btfrv.h index 9cd7476..1e85cb0 100644 --- a/src/btfrv.h +++ b/src/btfrv.h | |||
| @@ -57,10 +57,25 @@ RvSocket RvServiceRegister(const char* relay_host, unsigned short port, | |||
| 57 | bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs = 0); | 57 | bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs = 0); |
| 58 | 58 | ||
| 59 | // ---- client side (the node dialing a .btf address) ---- | 59 | // ---- client side (the node dialing a .btf address) ---- |
| 60 | // Where a failed RvClientConnect stopped. RV_INVALID alone collapses three | ||
| 61 | // different situations -- relay unreachable, relay reachable but nobody | ||
| 62 | // registered, connection died mid-pairing -- and they point at different | ||
| 63 | // components. The caller can only report the right one if this side says | ||
| 64 | // which it saw. | ||
| 65 | enum RvClientFail | ||
| 66 | { | ||
| 67 | RVC_FAIL_NONE = 0, | ||
| 68 | RVC_FAIL_DIAL, // TCP connect to the relay itself failed | ||
| 69 | RVC_FAIL_REFUSED, // relay answered 0x00: no service registered under this pubkey | ||
| 70 | RVC_FAIL_PAIR, // relay connection died before pairing completed | ||
| 71 | }; | ||
| 72 | |||
| 60 | // Connect through the relay to the service registered under `target_pubkey`. | 73 | // Connect through the relay to the service registered under `target_pubkey`. |
| 61 | // Returns the paired socket, or RV_INVALID on error. | 74 | // Returns the paired socket, or RV_INVALID on error -- with `pFail`, if |
| 75 | // given, naming the stage that failed. | ||
| 62 | RvSocket RvClientConnect(const char* relay_host, unsigned short port, | 76 | RvSocket RvClientConnect(const char* relay_host, unsigned short port, |
| 63 | const unsigned char target_pubkey[32]); | 77 | const unsigned char target_pubkey[32], |
| 78 | RvClientFail* pFail = 0); | ||
| 64 | 79 | ||
| 65 | // Blocking read/write of exactly n bytes over a rendezvous socket. Return false | 80 | // Blocking read/write of exactly n bytes over a rendezvous socket. Return false |
| 66 | // on error/EOF. | 81 | // on error/EOF. |
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp index 63d178b..4058f6b 100644 --- a/src/btftunnel.cpp +++ b/src/btftunnel.cpp | |||
| @@ -25,8 +25,12 @@ typedef int SOCKET; | |||
| 25 | #endif | 25 | #endif |
| 26 | #include "sockcount.h" | 26 | #include "sockcount.h" |
| 27 | #include <cstring> | 27 | #include <cstring> |
| 28 | #include <cstdio> | ||
| 29 | #include <memory> | ||
| 30 | #include <new> | ||
| 28 | #include <vector> | 31 | #include <vector> |
| 29 | #include <thread> | 32 | #include <thread> |
| 33 | #include <system_error> | ||
| 30 | 34 | ||
| 31 | namespace btf | 35 | namespace btf |
| 32 | { | 36 | { |
| @@ -119,35 +123,155 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key) | |||
| 119 | shutdown((SOCKET)rv, SD_BOTH); | 123 | shutdown((SOCKET)rv, SD_BOTH); |
| 120 | } | 124 | } |
| 121 | 125 | ||
| 126 | // The pump end and the rendezvous socket, owned jointly by whoever is still | ||
| 127 | // using them. Closed once, by the last reference to go. | ||
| 128 | // | ||
| 129 | // Joint ownership rather than "the inbound pump owns the cleanup", because | ||
| 130 | // either pump can reach its exit first and BOTH touch BOTH descriptors on the | ||
| 131 | // way out -- PumpOut ends in shutdown(rv), shutdown(pumpEnd) and PumpIn ends in | ||
| 132 | // the mirror image. A single owner therefore has no exit at which the other is | ||
| 133 | // guaranteed to be finished. Holding a reference for the whole body of each | ||
| 134 | // pump gives it one: the close cannot happen until the last pump has stopped | ||
| 135 | // touching the descriptors, so no shutdown() can land on a number that has | ||
| 136 | // already been closed and handed back out by the kernel to an unrelated | ||
| 137 | // connection. | ||
| 138 | struct TunnelEnds | ||
| 139 | { | ||
| 140 | SOCKET pumpEnd; | ||
| 141 | RvSocket rv; | ||
| 142 | TunnelEnds(SOCKET p, RvSocket r) : pumpEnd(p), rv(r) {} | ||
| 143 | ~TunnelEnds() { BtfCloseSocket(pumpEnd); RvClose(rv); } | ||
| 144 | private: | ||
| 145 | TunnelEnds(const TunnelEnds&); | ||
| 146 | TunnelEnds& operator=(const TunnelEnds&); | ||
| 147 | }; | ||
| 148 | |||
| 122 | // Start the two pumps and hand back the app-facing socket. When both pumps | 149 | // Start the two pumps and hand back the app-facing socket. When both pumps |
| 123 | // finish, close the pump end and the rendezvous socket. | 150 | // finish, close the pump end and the rendezvous socket. |
| 151 | // | ||
| 152 | // That sentence used to be a comment describing something the code did not do. | ||
| 153 | // The inbound pump closed both descriptors as soon as it exited, without | ||
| 154 | // waiting for the outbound pump, which then called shutdown() on both at its | ||
| 155 | // own exit. Whether that shutdown hit a live descriptor or a closed one came | ||
| 156 | // down to which pump got there first, and the rendezvous socket dying before | ||
| 157 | // the app socket -- a relay restart, the far side hanging up -- is precisely | ||
| 158 | // the order that puts the inbound pump ahead. Measured over 25 such teardowns | ||
| 159 | // against the real code: 29 shutdowns landed on an already-closed descriptor. | ||
| 160 | // | ||
| 161 | // appEnd is not part of this. It is handed to the caller on success and closed | ||
| 162 | // here on every failure path; no pump ever touches it. | ||
| 163 | // | ||
| 164 | // Every tunnel is two threads, and a tunnel is one peer connection (a .btf | ||
| 165 | // dial or accept, a pool miner, a participant's pool session) -- so the | ||
| 166 | // thread count scales with peer count, which is attacker-influenced. When | ||
| 167 | // pthread_create is refused std::thread throws std::system_error; uncaught it | ||
| 168 | // reached std::terminate and took the whole process down over one connection | ||
| 169 | // it could not serve. Both spawns are guarded, and the failure paths return | ||
| 170 | // INVALID_SOCKET, which every caller already handles by dropping just that | ||
| 171 | // connection. | ||
| 124 | static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) | 172 | static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) |
| 125 | { | 173 | { |
| 126 | btf_socket_t appEnd, pumpEnd; | 174 | btf_socket_t appEnd, pumpEnd; |
| 127 | if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } | 175 | if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } |
| 128 | 176 | ||
| 129 | std::vector<unsigned char> key(shared, shared + 32); | 177 | std::vector<unsigned char> key(shared, shared + 32); |
| 130 | std::thread(PumpOut, pumpEnd, rv, key).detach(); | 178 | |
| 131 | std::thread([pumpEnd, rv, key]() { | 179 | // make_shared, not shared_ptr(new ...): it allocates before it constructs, |
| 132 | PumpIn(pumpEnd, rv, key); | 180 | // so it either comes back owning both descriptors or throws having taken |
| 181 | // ownership of neither. The two-step form can run the deleter and then | ||
| 182 | // throw, which would close both here and again below. | ||
| 183 | std::shared_ptr<TunnelEnds> ends; | ||
| 184 | try | ||
| 185 | { | ||
| 186 | ends = std::make_shared<TunnelEnds>(pumpEnd, rv); | ||
| 187 | } | ||
| 188 | catch (const std::bad_alloc&) | ||
| 189 | { | ||
| 190 | fprintf(stderr, "btf tunnel: out of memory starting tunnel, tunnel dropped\n"); | ||
| 133 | BtfCloseSocket(pumpEnd); | 191 | BtfCloseSocket(pumpEnd); |
| 192 | BtfCloseSocket(appEnd); | ||
| 134 | RvClose(rv); | 193 | RvClose(rv); |
| 135 | }).detach(); | 194 | return INVALID_SOCKET; |
| 195 | } | ||
| 196 | |||
| 197 | try | ||
| 198 | { | ||
| 199 | std::thread([ends, key]() { PumpIn(ends->pumpEnd, ends->rv, key); }).detach(); | ||
| 200 | } | ||
| 201 | catch (const std::system_error& e) | ||
| 202 | { | ||
| 203 | // Nothing is running yet: `ends` is the only reference, and returning | ||
| 204 | // drops it, which is what closes pumpEnd and rv. | ||
| 205 | fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n", | ||
| 206 | e.what()); | ||
| 207 | BtfCloseSocket(appEnd); | ||
| 208 | return INVALID_SOCKET; | ||
| 209 | } | ||
| 210 | |||
| 211 | try | ||
| 212 | { | ||
| 213 | std::thread([ends, key]() { PumpOut(ends->pumpEnd, ends->rv, key); }).detach(); | ||
| 214 | } | ||
| 215 | catch (const std::system_error& e) | ||
| 216 | { | ||
| 217 | // The inbound pump may be parked in RvReadN(rv) or may already have | ||
| 218 | // exited -- when the spawn is refused because the process is loaded, | ||
| 219 | // the rendezvous socket is often dead for the same reason, and then | ||
| 220 | // PumpIn returns on its first length read. Either way this thread | ||
| 221 | // still holds a reference, so both descriptors are still open: the | ||
| 222 | // shutdown wakes the pump if it is parked and is harmless if it is | ||
| 223 | // not. The close belongs to whichever reference goes last. | ||
| 224 | fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n", | ||
| 225 | e.what()); | ||
| 226 | shutdown((SOCKET)ends->rv, SD_BOTH); | ||
| 227 | shutdown(ends->pumpEnd, SD_BOTH); | ||
| 228 | BtfCloseSocket(appEnd); | ||
| 229 | return INVALID_SOCKET; | ||
| 230 | } | ||
| 136 | return appEnd; | 231 | return appEnd; |
| 137 | } | 232 | } |
| 138 | 233 | ||
| 234 | const char* TunnelFailText(TunnelFail f) | ||
| 235 | { | ||
| 236 | switch (f) | ||
| 237 | { | ||
| 238 | case TUNNEL_FAIL_NONE: return "no failure"; | ||
| 239 | case TUNNEL_FAIL_INIT: return "local socket/crypto init failed"; | ||
| 240 | case TUNNEL_FAIL_DIAL: return "meeting node unreachable (tcp dial failed)"; | ||
| 241 | case TUNNEL_FAIL_REFUSED: return "relay reachable, but no service registered there for this address"; | ||
| 242 | case TUNNEL_FAIL_PAIR: return "relay connection died before pairing completed"; | ||
| 243 | case TUNNEL_FAIL_HANDSHAKE: return "end-to-end handshake failed"; | ||
| 244 | case TUNNEL_FAIL_LOCAL: return "local tunnel setup failed"; | ||
| 245 | } | ||
| 246 | return "unknown failure"; | ||
| 247 | } | ||
| 248 | |||
| 139 | btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, | 249 | btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, |
| 140 | const unsigned char target_pubkey[32], | 250 | const unsigned char target_pubkey[32], |
| 141 | const unsigned char service_enc_pub[32]) | 251 | const unsigned char service_enc_pub[32], |
| 252 | TunnelFail* pFail) | ||
| 142 | { | 253 | { |
| 254 | if (pFail) *pFail = TUNNEL_FAIL_INIT; | ||
| 143 | if (!TunnelInit()) return INVALID_SOCKET; | 255 | if (!TunnelInit()) return INVALID_SOCKET; |
| 144 | RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey); | 256 | RvClientFail rvFail = RVC_FAIL_NONE; |
| 145 | if (rv == RV_INVALID) return INVALID_SOCKET; | 257 | RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey, &rvFail); |
| 258 | if (rv == RV_INVALID) | ||
| 259 | { | ||
| 260 | if (pFail) | ||
| 261 | *pFail = rvFail == RVC_FAIL_DIAL ? TUNNEL_FAIL_DIAL | ||
| 262 | : rvFail == RVC_FAIL_REFUSED ? TUNNEL_FAIL_REFUSED | ||
| 263 | : TUNNEL_FAIL_PAIR; | ||
| 264 | return INVALID_SOCKET; | ||
| 265 | } | ||
| 146 | 266 | ||
| 147 | unsigned char eph_pub[32], shared[32]; | 267 | unsigned char eph_pub[32], shared[32]; |
| 268 | if (pFail) *pFail = TUNNEL_FAIL_HANDSHAKE; | ||
| 148 | if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; } | 269 | if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; } |
| 149 | if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; } | 270 | if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; } |
| 150 | return StartTunnel(rv, shared); | 271 | if (pFail) *pFail = TUNNEL_FAIL_LOCAL; |
| 272 | btf_socket_t app = StartTunnel(rv, shared); | ||
| 273 | if (app != INVALID_SOCKET && pFail) *pFail = TUNNEL_FAIL_NONE; | ||
| 274 | return app; | ||
| 151 | } | 275 | } |
| 152 | 276 | ||
| 153 | btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]) | 277 | btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]) |
diff --git a/src/btftunnel.h b/src/btftunnel.h index 24a7509..3cc87cd 100644 --- a/src/btftunnel.h +++ b/src/btftunnel.h | |||
| @@ -36,12 +36,33 @@ namespace btf | |||
| 36 | // Initialize (sockets + libsodium). Idempotent. | 36 | // Initialize (sockets + libsodium). Idempotent. |
| 37 | bool TunnelInit(); | 37 | bool TunnelInit(); |
| 38 | 38 | ||
| 39 | // Which stage of BtfClientTunnel failed. INVALID_SOCKET alone says only "no | ||
| 40 | // tunnel"; a caller that then guesses at causes in its log line sends whoever | ||
| 41 | // reads it to the wrong component (issue #17: "relay unreachable or pool not | ||
| 42 | // listening" blamed two healthy parts). The stage is what this side actually | ||
| 43 | // knows -- report that. | ||
| 44 | enum TunnelFail | ||
| 45 | { | ||
| 46 | TUNNEL_FAIL_NONE = 0, | ||
| 47 | TUNNEL_FAIL_INIT, // local socket/crypto init failed | ||
| 48 | TUNNEL_FAIL_DIAL, // TCP connect to the meeting node failed | ||
| 49 | TUNNEL_FAIL_REFUSED, // relay reachable, but no service registered under the pubkey | ||
| 50 | TUNNEL_FAIL_PAIR, // relay connection died before pairing completed | ||
| 51 | TUNNEL_FAIL_HANDSHAKE, // end-to-end channel handshake failed | ||
| 52 | TUNNEL_FAIL_LOCAL, // local pump plumbing (socket pair / threads) failed | ||
| 53 | }; | ||
| 54 | |||
| 55 | // Short description of a TunnelFail for log lines. | ||
| 56 | const char* TunnelFailText(TunnelFail f); | ||
| 57 | |||
| 39 | // Client: dial the service registered under `target_pubkey` at the meeting node | 58 | // Client: dial the service registered under `target_pubkey` at the meeting node |
| 40 | // (host:port), box to its static x25519 `service_enc_pub`, and return a local | 59 | // (host:port), box to its static x25519 `service_enc_pub`, and return a local |
| 41 | // plaintext SOCKET tunneled end-to-end to the service. INVALID_SOCKET on error. | 60 | // plaintext SOCKET tunneled end-to-end to the service. INVALID_SOCKET on error |
| 61 | // -- with `pFail`, if given, naming the stage that failed. | ||
| 42 | btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, | 62 | btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, |
| 43 | const unsigned char target_pubkey[32], | 63 | const unsigned char target_pubkey[32], |
| 44 | const unsigned char service_enc_pub[32]); | 64 | const unsigned char service_enc_pub[32], |
| 65 | TunnelFail* pFail = 0); | ||
| 45 | 66 | ||
| 46 | // Service: wrap an already-paired rendezvous socket (RvServiceRegister followed | 67 | // Service: wrap an already-paired rendezvous socket (RvServiceRegister followed |
| 47 | // by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with | 68 | // by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with |
| @@ -3,6 +3,7 @@ | |||
| 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. | 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. |
| 4 | 4 | ||
| 5 | #include "headers.h" | 5 | #include "headers.h" |
| 6 | #include "wallet_sqlite.h" | ||
| 6 | 7 | ||
| 7 | 8 | ||
| 8 | 9 | ||
| @@ -52,8 +53,34 @@ static bool fDbEnvInit = false; | |||
| 52 | // Why LoadWallet() refused, in words meant for the person who has to act on it. | 53 | // Why LoadWallet() refused, in words meant for the person who has to act on it. |
| 53 | // Empty unless the reason is one we can name. | 54 | // Empty unless the reason is one we can name. |
| 54 | string strWalletLoadError; | 55 | string strWalletLoadError; |
| 55 | DbEnv dbenv(0u); | 56 | static DbEnv* pdbenv = NULL; |
| 57 | static FILE* pdbErrFile = NULL; | ||
| 56 | static map<string, int> mapFileUseCount; | 58 | static map<string, int> mapFileUseCount; |
| 59 | static CWalletDBSQLite* pWalletSQLiteRuntime = NULL; | ||
| 60 | |||
| 61 | DbEnv& GetDbEnv() | ||
| 62 | { | ||
| 63 | if (!pdbenv) | ||
| 64 | pdbenv = new DbEnv(0u); | ||
| 65 | return *pdbenv; | ||
| 66 | } | ||
| 67 | |||
| 68 | static void CloseDbEnv() | ||
| 69 | { | ||
| 70 | if (pdbenv) | ||
| 71 | { | ||
| 72 | try { pdbenv->close(0); } | ||
| 73 | catch (...) { } | ||
| 74 | delete pdbenv; | ||
| 75 | pdbenv = NULL; | ||
| 76 | } | ||
| 77 | if (pdbErrFile) | ||
| 78 | { | ||
| 79 | fclose(pdbErrFile); | ||
| 80 | pdbErrFile = NULL; | ||
| 81 | } | ||
| 82 | fDbEnvInit = false; | ||
| 83 | } | ||
| 57 | 84 | ||
| 58 | class CDBInit | 85 | class CDBInit |
| 59 | { | 86 | { |
| @@ -64,15 +91,189 @@ public: | |||
| 64 | ~CDBInit() | 91 | ~CDBInit() |
| 65 | { | 92 | { |
| 66 | if (fDbEnvInit) | 93 | if (fDbEnvInit) |
| 67 | { | 94 | CloseDbEnv(); |
| 68 | try { dbenv.close(0); } | ||
| 69 | catch (...) { } | ||
| 70 | fDbEnvInit = false; | ||
| 71 | } | ||
| 72 | } | 95 | } |
| 73 | } | 96 | } |
| 74 | instance_of_cdbinit; | 97 | instance_of_cdbinit; |
| 75 | 98 | ||
| 99 | bool WalletSQLiteRuntimeActive() | ||
| 100 | { | ||
| 101 | return pWalletSQLiteRuntime != NULL; | ||
| 102 | } | ||
| 103 | |||
| 104 | bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist) | ||
| 105 | { | ||
| 106 | strErrorRet.clear(); | ||
| 107 | // Callers that load an existing wallet want the clear "does not exist" | ||
| 108 | // message rather than silently creating a blank one; the create path passes | ||
| 109 | // fMustExist=false so Open() (SQLITE_OPEN_CREATE) can make a fresh file. | ||
| 110 | if (fMustExist && !FileExists(strPath.c_str())) | ||
| 111 | { | ||
| 112 | strErrorRet = strprintf("SQLite wallet does not exist: %s", | ||
| 113 | strPath.c_str()); | ||
| 114 | return false; | ||
| 115 | } | ||
| 116 | |||
| 117 | WalletSQLiteRuntimeClose(); | ||
| 118 | std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite()); | ||
| 119 | if (!db->Open(strPath, strErrorRet)) | ||
| 120 | return false; | ||
| 121 | |||
| 122 | pWalletSQLiteRuntime = db.release(); | ||
| 123 | return true; | ||
| 124 | } | ||
| 125 | |||
| 126 | void WalletSQLiteRuntimeClose() | ||
| 127 | { | ||
| 128 | if (pWalletSQLiteRuntime) | ||
| 129 | { | ||
| 130 | // Fold the write-ahead log into the main file on the way out. WAL + | ||
| 131 | // synchronous=FULL already makes every committed write durable across a | ||
| 132 | // kill, so this is housekeeping, not the thing that protects the data: | ||
| 133 | // if it fails, or the node is killed before it runs, the wallet is | ||
| 134 | // still whole. Best effort, never throws. | ||
| 135 | string strCheckpointError; | ||
| 136 | pWalletSQLiteRuntime->Checkpoint(strCheckpointError); | ||
| 137 | delete pWalletSQLiteRuntime; | ||
| 138 | pWalletSQLiteRuntime = NULL; | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey, | ||
| 143 | vector<unsigned char>& vchValueRet) | ||
| 144 | { | ||
| 145 | if (!pWalletSQLiteRuntime) | ||
| 146 | return false; | ||
| 147 | string strError; | ||
| 148 | return pWalletSQLiteRuntime->ReadRecord(vchKey, vchValueRet, strError); | ||
| 149 | } | ||
| 150 | |||
| 151 | bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey, | ||
| 152 | const vector<unsigned char>& vchValue, | ||
| 153 | bool fOverwrite) | ||
| 154 | { | ||
| 155 | if (!pWalletSQLiteRuntime) | ||
| 156 | return false; | ||
| 157 | string strError; | ||
| 158 | return pWalletSQLiteRuntime->WriteRecord(vchKey, vchValue, strError, | ||
| 159 | fOverwrite); | ||
| 160 | } | ||
| 161 | |||
| 162 | bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey) | ||
| 163 | { | ||
| 164 | if (!pWalletSQLiteRuntime) | ||
| 165 | return false; | ||
| 166 | string strError; | ||
| 167 | return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); | ||
| 168 | } | ||
| 169 | |||
| 170 | // Transaction bracket for the SQLite runtime. When the SQLite backend is not | ||
| 171 | // active these are no-ops that succeed, so a caller can wrap a group of related | ||
| 172 | // writes and Berkeley DB keeps its existing per-write behaviour unchanged while | ||
| 173 | // SQLite gets all-or-nothing. | ||
| 174 | bool WalletSQLiteRuntimeBeginTxn() | ||
| 175 | { | ||
| 176 | if (!pWalletSQLiteRuntime) | ||
| 177 | return true; | ||
| 178 | string strError; | ||
| 179 | if (pWalletSQLiteRuntime->BeginTransaction(strError)) | ||
| 180 | return true; | ||
| 181 | printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str()); | ||
| 182 | return false; | ||
| 183 | } | ||
| 184 | |||
| 185 | bool WalletSQLiteRuntimeCommitTxn() | ||
| 186 | { | ||
| 187 | if (!pWalletSQLiteRuntime) | ||
| 188 | return true; | ||
| 189 | string strError; | ||
| 190 | if (pWalletSQLiteRuntime->CommitTransaction(strError)) | ||
| 191 | return true; | ||
| 192 | printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str()); | ||
| 193 | return false; | ||
| 194 | } | ||
| 195 | |||
| 196 | void WalletSQLiteRuntimeRollbackTxn() | ||
| 197 | { | ||
| 198 | if (!pWalletSQLiteRuntime) | ||
| 199 | return; | ||
| 200 | string strError; | ||
| 201 | pWalletSQLiteRuntime->RollbackTransaction(strError); | ||
| 202 | } | ||
| 203 | |||
| 204 | // Open the SQLite wallet read-only as the active runtime. Used by the one-shot | ||
| 205 | // storage diagnostics, which run before the normal wallet load and must be able | ||
| 206 | // to inspect a wallet that the loader would refuse -- so this opens the file for | ||
| 207 | // scanning without loading or validating its records. | ||
| 208 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet) | ||
| 209 | { | ||
| 210 | strErrorRet.clear(); | ||
| 211 | if (!FileExists(strPath.c_str())) | ||
| 212 | { | ||
| 213 | strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str()); | ||
| 214 | return false; | ||
| 215 | } | ||
| 216 | |||
| 217 | WalletSQLiteRuntimeClose(); | ||
| 218 | std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite()); | ||
| 219 | if (!db->OpenReadOnly(strPath, strErrorRet)) | ||
| 220 | return false; | ||
| 221 | |||
| 222 | pWalletSQLiteRuntime = db.release(); | ||
| 223 | return true; | ||
| 224 | } | ||
| 225 | |||
| 226 | bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet) | ||
| 227 | { | ||
| 228 | if (!pWalletSQLiteRuntime) | ||
| 229 | { | ||
| 230 | strErrorRet = "SQLite wallet backend is not active"; | ||
| 231 | return false; | ||
| 232 | } | ||
| 233 | return pWalletSQLiteRuntime->Checkpoint(strErrorRet); | ||
| 234 | } | ||
| 235 | |||
| 236 | // The persistent record of which backend this datadir uses, so the choice | ||
| 237 | // survives a restart without a command-line flag. A plain text file holding | ||
| 238 | // "sqlite" or "bdb" -- visible and hand-editable on purpose, no hidden state. | ||
| 239 | static string WalletBackendMarkerPath() | ||
| 240 | { | ||
| 241 | return GetAppDir() + "/wallet-backend"; | ||
| 242 | } | ||
| 243 | |||
| 244 | string ReadWalletBackendMarker() | ||
| 245 | { | ||
| 246 | FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "rb"); | ||
| 247 | if (!pf) | ||
| 248 | return ""; | ||
| 249 | char buf[16] = {0}; | ||
| 250 | size_t n = fread(buf, 1, sizeof(buf) - 1, pf); | ||
| 251 | fclose(pf); | ||
| 252 | string s(buf, n); | ||
| 253 | while (!s.empty() && isspace((unsigned char)s[s.size() - 1])) | ||
| 254 | s.erase(s.size() - 1); | ||
| 255 | while (!s.empty() && isspace((unsigned char)s[0])) | ||
| 256 | s.erase(0, 1); | ||
| 257 | if (s == "sqlite" || s == "bdb") | ||
| 258 | return s; | ||
| 259 | return ""; | ||
| 260 | } | ||
| 261 | |||
| 262 | bool WriteWalletBackendMarker(const string& strBackend) | ||
| 263 | { | ||
| 264 | if (strBackend != "sqlite" && strBackend != "bdb") | ||
| 265 | return false; | ||
| 266 | FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "wb"); | ||
| 267 | if (!pf) | ||
| 268 | return false; | ||
| 269 | bool fOk = fwrite(strBackend.c_str(), 1, strBackend.size(), pf) == strBackend.size(); | ||
| 270 | if (fputc('\n', pf) == EOF) | ||
| 271 | fOk = false; | ||
| 272 | if (fclose(pf) != 0) | ||
| 273 | fOk = false; | ||
| 274 | return fOk; | ||
| 275 | } | ||
| 276 | |||
| 76 | 277 | ||
| 77 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | 278 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) |
| 78 | { | 279 | { |
| @@ -104,24 +305,30 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | |||
| 104 | 305 | ||
| 105 | printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); | 306 | printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); |
| 106 | 307 | ||
| 107 | dbenv.set_lg_dir(strLogDir.c_str()); | 308 | DbEnv& env = GetDbEnv(); |
| 108 | dbenv.set_lg_max(10000000); | 309 | env.set_lg_dir(strLogDir.c_str()); |
| 109 | dbenv.set_lk_max_locks(10000); | 310 | env.set_lg_max(10000000); |
| 110 | dbenv.set_lk_max_objects(10000); | 311 | env.set_lk_max_locks(10000); |
| 111 | dbenv.set_errfile(fopen("db.log", "a")); /// debug | 312 | env.set_lk_max_objects(10000); |
| 112 | ///dbenv.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption | 313 | pdbErrFile = fopen("db.log", "a"); /// debug |
| 113 | ret = dbenv.open(strAppDir.c_str(), | 314 | if (pdbErrFile) |
| 114 | DB_CREATE | | 315 | env.set_errfile(pdbErrFile); |
| 115 | DB_INIT_LOCK | | 316 | ///env.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption |
| 116 | DB_INIT_LOG | | 317 | ret = env.open(strAppDir.c_str(), |
| 117 | DB_INIT_MPOOL | | 318 | DB_CREATE | |
| 118 | DB_INIT_TXN | | 319 | DB_INIT_LOCK | |
| 119 | DB_THREAD | | 320 | DB_INIT_LOG | |
| 120 | DB_PRIVATE | | 321 | DB_INIT_MPOOL | |
| 121 | DB_RECOVER, | 322 | DB_INIT_TXN | |
| 122 | 0); | 323 | DB_THREAD | |
| 324 | DB_PRIVATE | | ||
| 325 | DB_RECOVER, | ||
| 326 | 0); | ||
| 123 | if (ret > 0) | 327 | if (ret > 0) |
| 328 | { | ||
| 329 | CloseDbEnv(); | ||
| 124 | throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); | 330 | throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); |
| 331 | } | ||
| 125 | fDbEnvInit = true; | 332 | fDbEnvInit = true; |
| 126 | 333 | ||
| 127 | // A Berkeley DB file records, in every page header, a log sequence | 334 | // A Berkeley DB file records, in every page header, a log sequence |
| @@ -151,7 +358,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | |||
| 151 | continue; | 358 | continue; |
| 152 | try | 359 | try |
| 153 | { | 360 | { |
| 154 | if (dbenv.lsn_reset(pszName, 0) == 0) | 361 | if (env.lsn_reset(pszName, 0) == 0) |
| 155 | printf("CDB() : adopted %s from another environment " | 362 | printf("CDB() : adopted %s from another environment " |
| 156 | "(log sequence numbers reset)\n", pszName); | 363 | "(log sequence numbers reset)\n", pszName); |
| 157 | } | 364 | } |
| @@ -164,7 +371,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | |||
| 164 | ++mapFileUseCount[strFile]; | 371 | ++mapFileUseCount[strFile]; |
| 165 | } | 372 | } |
| 166 | 373 | ||
| 167 | pdb = new Db(&dbenv, 0); | 374 | pdb = new Db(&GetDbEnv(), 0); |
| 168 | 375 | ||
| 169 | ret = pdb->open(NULL, // Txn pointer | 376 | ret = pdb->open(NULL, // Txn pointer |
| 170 | pszFile, // Filename | 377 | pszFile, // Filename |
| @@ -199,7 +406,8 @@ void CDB::Close() | |||
| 199 | try { pdb->close(0); } catch (...) { } | 406 | try { pdb->close(0); } catch (...) { } |
| 200 | delete pdb; | 407 | delete pdb; |
| 201 | pdb = NULL; | 408 | pdb = NULL; |
| 202 | try { dbenv.txn_checkpoint(0, 0, 0); } catch (...) { } | 409 | if (fDbEnvInit) |
| 410 | try { GetDbEnv().txn_checkpoint(0, 0, 0); } catch (...) { } | ||
| 203 | 411 | ||
| 204 | CRITICAL_BLOCK(cs_db) | 412 | CRITICAL_BLOCK(cs_db) |
| 205 | --mapFileUseCount[strFile]; | 413 | --mapFileUseCount[strFile]; |
| @@ -279,7 +487,8 @@ void DBFlush(bool fShutdown) | |||
| 279 | // takes the process down before the wallet has been put down cleanly, | 487 | // takes the process down before the wallet has been put down cleanly, |
| 280 | // which is the difference between a portable wallet.dat and one welded | 488 | // which is the difference between a portable wallet.dat and one welded |
| 281 | // to this directory -- the lsn_reset just below is what frees it. | 489 | // to this directory -- the lsn_reset just below is what frees it. |
| 282 | try { dbenv.txn_checkpoint(0, 0, 0); } | 490 | DbEnv& env = GetDbEnv(); |
| 491 | try { env.txn_checkpoint(0, 0, 0); } | ||
| 283 | catch (const std::exception& e) | 492 | catch (const std::exception& e) |
| 284 | { printf("DBFlush() : checkpoint failed: %s\n", e.what()); } | 493 | { printf("DBFlush() : checkpoint failed: %s\n", e.what()); } |
| 285 | 494 | ||
| @@ -290,7 +499,7 @@ void DBFlush(bool fShutdown) | |||
| 290 | int nRefCount = (*mi).second; | 499 | int nRefCount = (*mi).second; |
| 291 | if (nRefCount == 0) | 500 | if (nRefCount == 0) |
| 292 | { | 501 | { |
| 293 | try { dbenv.lsn_reset(strFile.c_str(), 0); } | 502 | try { env.lsn_reset(strFile.c_str(), 0); } |
| 294 | catch (const std::exception& e) | 503 | catch (const std::exception& e) |
| 295 | { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); } | 504 | { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); } |
| 296 | mapFileUseCount.erase(mi++); | 505 | mapFileUseCount.erase(mi++); |
| @@ -310,9 +519,8 @@ void DBFlush(bool fShutdown) | |||
| 310 | // Found while making wallet encryption clean up after itself: | 519 | // Found while making wallet encryption clean up after itself: |
| 311 | // calling DBFlush(true) a second time crashed here every time. | 520 | // calling DBFlush(true) a second time crashed here every time. |
| 312 | if (mapFileUseCount.empty()) | 521 | if (mapFileUseCount.empty()) |
| 313 | try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } | 522 | try { env.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } |
| 314 | try { dbenv.close(0); } catch (...) { } | 523 | CloseDbEnv(); |
| 315 | fDbEnvInit = false; | ||
| 316 | } | 524 | } |
| 317 | } | 525 | } |
| 318 | } | 526 | } |
| @@ -614,6 +822,294 @@ bool CTxDB::LoadBlockIndex() | |||
| 614 | // CWalletDB | 822 | // CWalletDB |
| 615 | // | 823 | // |
| 616 | 824 | ||
| 825 | CWalletDB::CWalletDB(const char* pszMode, bool fTxn) : | ||
| 826 | CDB(WalletSQLiteRuntimeActive() ? NULL : "wallet.dat", pszMode, fTxn) | ||
| 827 | { | ||
| 828 | } | ||
| 829 | |||
| 830 | class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor | ||
| 831 | { | ||
| 832 | public: | ||
| 833 | bool fHaveStoredMineMode; | ||
| 834 | bool fHaveStoredHDCoinType; | ||
| 835 | string strLastType; | ||
| 836 | |||
| 837 | CSQLiteWalletRuntimeLoadVisitor() : | ||
| 838 | fHaveStoredMineMode(false), | ||
| 839 | fHaveStoredHDCoinType(false) | ||
| 840 | { | ||
| 841 | } | ||
| 842 | |||
| 843 | bool VisitWalletRecord(const CDataStream& ssKeyIn, | ||
| 844 | const CDataStream& ssValueIn, | ||
| 845 | string& strErrorRet) | ||
| 846 | { | ||
| 847 | CDataStream ssKey = ssKeyIn; | ||
| 848 | CDataStream ssValue = ssValueIn; | ||
| 849 | string strType; | ||
| 850 | ssKey >> strType; | ||
| 851 | strLastType = strType; | ||
| 852 | |||
| 853 | if (strType == "name") | ||
| 854 | { | ||
| 855 | string strAddress; | ||
| 856 | ssKey >> strAddress; | ||
| 857 | ssValue >> mapAddressBook[strAddress]; | ||
| 858 | } | ||
| 859 | else if (strType == "tx") | ||
| 860 | { | ||
| 861 | uint256 hash; | ||
| 862 | ssKey >> hash; | ||
| 863 | CWalletTx& wtx = mapWallet[hash]; | ||
| 864 | ssValue >> wtx; | ||
| 865 | |||
| 866 | if (wtx.GetHash() != hash) | ||
| 867 | printf("Error in SQLite wallet export, hash mismatch\n"); | ||
| 868 | } | ||
| 869 | else if (strType == "key") | ||
| 870 | { | ||
| 871 | vector<unsigned char> vchPubKey; | ||
| 872 | ssKey >> vchPubKey; | ||
| 873 | CPrivKey vchPrivKey; | ||
| 874 | ssValue >> vchPrivKey; | ||
| 875 | |||
| 876 | mapKeys[vchPubKey] = vchPrivKey; | ||
| 877 | mapPubKeys[Hash160(vchPubKey)] = vchPubKey; | ||
| 878 | } | ||
| 879 | else if (strType == "mkey") | ||
| 880 | { | ||
| 881 | unsigned int nID = 0; | ||
| 882 | ssKey >> nID; | ||
| 883 | CWalletMasterKey kMasterKey; | ||
| 884 | ssValue >> kMasterKey; | ||
| 885 | mapMasterKeys[nID] = kMasterKey; | ||
| 886 | nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID); | ||
| 887 | fWalletEncrypted = true; | ||
| 888 | fWalletLocked = true; | ||
| 889 | } | ||
| 890 | else if (strType == "ckey") | ||
| 891 | { | ||
| 892 | vector<unsigned char> vchPubKey; | ||
| 893 | ssKey >> vchPubKey; | ||
| 894 | vector<unsigned char> vchCryptedSecret; | ||
| 895 | ssValue >> vchCryptedSecret; | ||
| 896 | if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) | ||
| 897 | { | ||
| 898 | strErrorRet = "SQLite wallet export has an unreadable encrypted key record"; | ||
| 899 | return false; | ||
| 900 | } | ||
| 901 | } | ||
| 902 | else if (strType == "defaultkey") | ||
| 903 | { | ||
| 904 | ssValue >> vchDefaultKey; | ||
| 905 | } | ||
| 906 | else if (strType == "hdmaster") | ||
| 907 | { | ||
| 908 | ssValue >> vchHDMaster; | ||
| 909 | } | ||
| 910 | else if (strType == "hdchaincode") | ||
| 911 | { | ||
| 912 | ssValue >> vchHDChainCode; | ||
| 913 | } | ||
| 914 | else if (strType == "hdnext") | ||
| 915 | { | ||
| 916 | ssValue >> nHDNext; | ||
| 917 | } | ||
| 918 | else if (strType == "hdschema") | ||
| 919 | { | ||
| 920 | ssValue >> nHDKeySchema; | ||
| 921 | } | ||
| 922 | else if (strType == "hdcointype") | ||
| 923 | { | ||
| 924 | ssValue >> nHDCoinType; | ||
| 925 | fHaveStoredHDCoinType = true; | ||
| 926 | } | ||
| 927 | else if (strType == "hdreceivenext") | ||
| 928 | { | ||
| 929 | ssValue >> nHDReceiveNext; | ||
| 930 | } | ||
| 931 | else if (strType == "hdchangenext") | ||
| 932 | { | ||
| 933 | ssValue >> nHDChangeNext; | ||
| 934 | } | ||
| 935 | else if (strType == "cryptedhdmaster") | ||
| 936 | { | ||
| 937 | ssValue >> vchCryptedHDMaster; | ||
| 938 | fWalletEncrypted = true; | ||
| 939 | fWalletLocked = true; | ||
| 940 | } | ||
| 941 | else if (strType == "cryptedhdchaincode") | ||
| 942 | { | ||
| 943 | ssValue >> vchCryptedHDChainCode; | ||
| 944 | fWalletEncrypted = true; | ||
| 945 | fWalletLocked = true; | ||
| 946 | } | ||
| 947 | else if (strType == "walletminversion") | ||
| 948 | { | ||
| 949 | int nMinVersion = 0; | ||
| 950 | ssValue >> nMinVersion; | ||
| 951 | if (nMinVersion > WALLET_FORMAT_SUPPORTED) | ||
| 952 | { | ||
| 953 | strWalletLoadError = strprintf( | ||
| 954 | "This SQLite wallet export was written by a newer version of " | ||
| 955 | "Bitflash (wallet format %d; this build understands %d). " | ||
| 956 | "Upgrade Bitflash before opening it.", | ||
| 957 | nMinVersion, WALLET_FORMAT_SUPPORTED); | ||
| 958 | strErrorRet = strWalletLoadError; | ||
| 959 | return false; | ||
| 960 | } | ||
| 961 | } | ||
| 962 | else if (strType == "pool") | ||
| 963 | { | ||
| 964 | int64 nIndex; | ||
| 965 | ssKey >> nIndex; | ||
| 966 | vector<unsigned char> vchPubKey; | ||
| 967 | ssValue >> vchPubKey; | ||
| 968 | mapKeyPool[nIndex] = vchPubKey; | ||
| 969 | } | ||
| 970 | else if (strType == "setting") | ||
| 971 | { | ||
| 972 | string strKey; | ||
| 973 | ssKey >> strKey; | ||
| 974 | if (strKey == "nTransactionFee") ssValue >> nTransactionFee; | ||
| 975 | if (strKey == "addrIncoming") ssValue >> addrIncoming; | ||
| 976 | |||
| 977 | if (!fMineModeFromCommandLine) | ||
| 978 | { | ||
| 979 | if (strKey == "nMineMode") { ssValue >> nMineMode; fHaveStoredMineMode = true; } | ||
| 980 | if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins; | ||
| 981 | if (strKey == "strParticipantPool") ssValue >> strParticipantPool; | ||
| 982 | } | ||
| 983 | } | ||
| 984 | |||
| 985 | if (ssKey.fail()) | ||
| 986 | { | ||
| 987 | strErrorRet = strprintf("malformed SQLite wallet record key while handling '%s'", | ||
| 988 | strLastType.c_str()); | ||
| 989 | return false; | ||
| 990 | } | ||
| 991 | if (ssValue.fail()) | ||
| 992 | { | ||
| 993 | strErrorRet = strprintf("malformed SQLite wallet record value while handling '%s'", | ||
| 994 | strLastType.c_str()); | ||
| 995 | return false; | ||
| 996 | } | ||
| 997 | return true; | ||
| 998 | } | ||
| 999 | |||
| 1000 | vector<unsigned char> vchDefaultKey; | ||
| 1001 | }; | ||
| 1002 | |||
| 1003 | static bool FinishLoadedWallet(const vector<unsigned char>& vchDefaultKey, | ||
| 1004 | bool fMayCreateDefaultKey) | ||
| 1005 | { | ||
| 1006 | if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) | ||
| 1007 | { | ||
| 1008 | keyUser.SetPubKey(vchDefaultKey); | ||
| 1009 | if (mapKeys.count(vchDefaultKey)) | ||
| 1010 | keyUser.SetPrivKey(mapKeys[vchDefaultKey]); | ||
| 1011 | } | ||
| 1012 | else if (IsWalletEncrypted()) | ||
| 1013 | { | ||
| 1014 | printf("LoadWallet: encrypted wallet has no usable default public key. " | ||
| 1015 | "The file was left untouched.\n"); | ||
| 1016 | return false; | ||
| 1017 | } | ||
| 1018 | else | ||
| 1019 | { | ||
| 1020 | if (!fMayCreateDefaultKey) | ||
| 1021 | { | ||
| 1022 | strWalletLoadError = | ||
| 1023 | "SQLite wallet export has no usable default public key"; | ||
| 1024 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1025 | return false; | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | keyUser.MakeNewKey(); | ||
| 1029 | if (!AddKey(keyUser)) | ||
| 1030 | return false; | ||
| 1031 | if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) | ||
| 1032 | return false; | ||
| 1033 | CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); | ||
| 1034 | } | ||
| 1035 | |||
| 1036 | return true; | ||
| 1037 | } | ||
| 1038 | |||
| 1039 | static bool ReconcileLoadedWalletMetadata(bool fHaveStoredMineMode, | ||
| 1040 | bool fHaveStoredHDCoinType) | ||
| 1041 | { | ||
| 1042 | // An encrypted wallet has a seed; it just has not been decrypted yet. | ||
| 1043 | // | ||
| 1044 | // The reset in the else branch was written when "no seed in memory after | ||
| 1045 | // loading" could only mean "this wallet has no seed". With encryption that | ||
| 1046 | // is no longer true, and taking the else branch here wiped the schema and | ||
| 1047 | // the derivation counters that had just been read correctly out of the | ||
| 1048 | // file. Unlocking later restores the seed but nothing restores those, so a | ||
| 1049 | // BIP44 wallet came back deriving legacy addresses -- different addresses | ||
| 1050 | // from the ones its own recovery phrase produces, while the audit reported | ||
| 1051 | // the phrase as covering nothing. | ||
| 1052 | if (HaveHDSeed() || !vchCryptedHDMaster.empty()) | ||
| 1053 | { | ||
| 1054 | if (nHDKeySchema == HD_SCHEMA_NONE) | ||
| 1055 | { | ||
| 1056 | nHDKeySchema = HD_SCHEMA_LEGACY; | ||
| 1057 | printf("LoadWallet: deterministic seed has no schema metadata; " | ||
| 1058 | "treating it as legacy m/index'\n"); | ||
| 1059 | } | ||
| 1060 | else if (nHDKeySchema != HD_SCHEMA_LEGACY && | ||
| 1061 | nHDKeySchema != HD_SCHEMA_BIP44) | ||
| 1062 | { | ||
| 1063 | printf("LoadWallet: deterministic wallet schema %d (%s) is not " | ||
| 1064 | "supported by this build\n", | ||
| 1065 | nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str()); | ||
| 1066 | return false; | ||
| 1067 | } | ||
| 1068 | if (!fHaveStoredHDCoinType) | ||
| 1069 | { | ||
| 1070 | nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; | ||
| 1071 | printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " | ||
| 1072 | "using Bitflash coin type %u\n", nHDCoinType); | ||
| 1073 | } | ||
| 1074 | } | ||
| 1075 | else | ||
| 1076 | { | ||
| 1077 | nHDKeySchema = HD_SCHEMA_NONE; | ||
| 1078 | nHDNext = 0; | ||
| 1079 | nHDReceiveNext = 0; | ||
| 1080 | nHDChangeNext = 0; | ||
| 1081 | nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; | ||
| 1082 | } | ||
| 1083 | |||
| 1084 | // fGenerateBitcoins and nMineMode only mean anything together, and a | ||
| 1085 | // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already | ||
| 1086 | // wrote fGenerateBitcoins when mining was toggled, years before this fork | ||
| 1087 | // had a mode at all. Restoring that lone flag put a node into the one state | ||
| 1088 | // the miner cannot act on -- generate on, mode relay -- so every miner | ||
| 1089 | // thread started and returned at the relay guard, and the machine sat there | ||
| 1090 | // looking like it was mining while hashing nothing. That happened to a | ||
| 1091 | // 32-core node on the first 1.2.11 start. | ||
| 1092 | // | ||
| 1093 | // So the flag is only believed when the mode it belongs to was stored with | ||
| 1094 | // it, and the pair is made coherent either way. | ||
| 1095 | if (!fMineModeFromCommandLine) | ||
| 1096 | { | ||
| 1097 | if (!fHaveStoredMineMode) | ||
| 1098 | { | ||
| 1099 | if (fGenerateBitcoins) | ||
| 1100 | printf("LoadWallet: ignoring a stored fGenerateBitcoins with no mining mode " | ||
| 1101 | "beside it -- it predates this setting and cannot be read on its own\n"); | ||
| 1102 | fGenerateBitcoins = 0; | ||
| 1103 | } | ||
| 1104 | else | ||
| 1105 | { | ||
| 1106 | fGenerateBitcoins = (nMineMode != MINE_RELAY) ? 1 : 0; | ||
| 1107 | } | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | return true; | ||
| 1111 | } | ||
| 1112 | |||
| 617 | bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | 1113 | bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) |
| 618 | { | 1114 | { |
| 619 | // Whether wallet.dat actually carried a mining mode, as opposed to just the | 1115 | // Whether wallet.dat actually carried a mining mode, as opposed to just the |
| @@ -924,34 +1420,269 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 924 | return true; | 1420 | return true; |
| 925 | } | 1421 | } |
| 926 | 1422 | ||
| 1423 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) | ||
| 1424 | { | ||
| 1425 | // Under the SQLite backend there is no Berkeley DB cursor to walk; scan the | ||
| 1426 | // active SQLite store instead. This is the single choke point every "read | ||
| 1427 | // every wallet record" caller goes through (storage audit and check among | ||
| 1428 | // them), so routing it here makes those honour -walletbackend=sqlite. | ||
| 1429 | if (WalletSQLiteRuntimeActive()) | ||
| 1430 | { | ||
| 1431 | strErrorRet.clear(); | ||
| 1432 | return pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); | ||
| 1433 | } | ||
| 1434 | |||
| 1435 | class CWalletScanDB : public CWalletDB | ||
| 1436 | { | ||
| 1437 | public: | ||
| 1438 | CWalletScanDB() : CWalletDB("r") { } | ||
| 1439 | using CDB::GetCursor; | ||
| 1440 | using CDB::ReadAtCursor; | ||
| 1441 | }; | ||
| 1442 | |||
| 1443 | strErrorRet.clear(); | ||
| 1444 | CWalletScanDB dbWallet; | ||
| 1445 | Dbc* pcursor = dbWallet.GetCursor(); | ||
| 1446 | if (!pcursor) | ||
| 1447 | { | ||
| 1448 | strErrorRet = "could not open a wallet cursor"; | ||
| 1449 | return false; | ||
| 1450 | } | ||
| 1451 | CAutoCursor cursorGuard(pcursor); | ||
| 1452 | |||
| 1453 | loop | ||
| 1454 | { | ||
| 1455 | CDataStream ssKey(SER_DISK); | ||
| 1456 | CDataStream ssValue(SER_DISK); | ||
| 1457 | int ret = dbWallet.ReadAtCursor(pcursor, ssKey, ssValue); | ||
| 1458 | if (ret == DB_NOTFOUND) | ||
| 1459 | break; | ||
| 1460 | if (ret != 0) | ||
| 1461 | { | ||
| 1462 | strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); | ||
| 1463 | return false; | ||
| 1464 | } | ||
| 1465 | if (!visitor.VisitWalletRecord(ssKey, ssValue, strErrorRet)) | ||
| 1466 | return false; | ||
| 1467 | } | ||
| 1468 | |||
| 1469 | return true; | ||
| 1470 | } | ||
| 1471 | |||
| 927 | bool LoadWallet() | 1472 | bool LoadWallet() |
| 928 | { | 1473 | { |
| 929 | vector<unsigned char> vchDefaultKey; | 1474 | vector<unsigned char> vchDefaultKey; |
| 930 | if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) | 1475 | if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) |
| 931 | return false; | 1476 | return false; |
| 932 | 1477 | ||
| 933 | if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) | 1478 | return FinishLoadedWallet(vchDefaultKey, true); |
| 1479 | } | ||
| 1480 | |||
| 1481 | bool LoadWalletFromSQLite(const string& strPath) | ||
| 1482 | { | ||
| 1483 | strWalletLoadError.clear(); | ||
| 1484 | |||
| 1485 | if (!FileExists(strPath.c_str())) | ||
| 934 | { | 1486 | { |
| 935 | // Set keyUser | 1487 | strWalletLoadError = strprintf("SQLite wallet export does not exist: %s", |
| 936 | keyUser.SetPubKey(vchDefaultKey); | 1488 | strPath.c_str()); |
| 937 | if (mapKeys.count(vchDefaultKey)) | 1489 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); |
| 938 | keyUser.SetPrivKey(mapKeys[vchDefaultKey]); | 1490 | return false; |
| 939 | } | 1491 | } |
| 940 | else if (IsWalletEncrypted()) | 1492 | |
| 1493 | CWalletDBSQLite sqlite; | ||
| 1494 | string strError; | ||
| 1495 | if (!sqlite.OpenReadOnly(strPath, strError)) | ||
| 941 | { | 1496 | { |
| 942 | printf("LoadWallet: encrypted wallet has no usable default public key. " | 1497 | strWalletLoadError = strError; |
| 943 | "The file was left untouched.\n"); | 1498 | printf("LoadWallet: cannot open SQLite wallet export: %s\n", |
| 1499 | strError.c_str()); | ||
| 944 | return false; | 1500 | return false; |
| 945 | } | 1501 | } |
| 946 | else | 1502 | |
| 1503 | CSQLiteWalletRuntimeLoadVisitor visitor; | ||
| 1504 | try | ||
| 947 | { | 1505 | { |
| 948 | // Create new keyUser and set as default key | 1506 | CRITICAL_BLOCK(cs_mapKeys) |
| 949 | keyUser.MakeNewKey(); | 1507 | CRITICAL_BLOCK(cs_mapWallet) |
| 950 | if (!AddKey(keyUser)) | 1508 | { |
| 951 | return false; | 1509 | if (!sqlite.ScanRecords(visitor, strError)) |
| 952 | if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) | 1510 | { |
| 953 | return false; | 1511 | if (strWalletLoadError.empty()) |
| 954 | CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); | 1512 | strWalletLoadError = strError; |
| 1513 | printf("LoadWallet: SQLite wallet export could not be read. " | ||
| 1514 | "Failed while handling a '%s' record: %s\n", | ||
| 1515 | visitor.strLastType.c_str(), strError.c_str()); | ||
| 1516 | return false; | ||
| 1517 | } | ||
| 1518 | } | ||
| 1519 | } | ||
| 1520 | catch (const std::exception& e) | ||
| 1521 | { | ||
| 1522 | strWalletLoadError = strprintf( | ||
| 1523 | "SQLite wallet export could not be read while handling a '%s' record: %s", | ||
| 1524 | visitor.strLastType.c_str(), e.what()); | ||
| 1525 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1526 | return false; | ||
| 1527 | } | ||
| 1528 | catch (...) | ||
| 1529 | { | ||
| 1530 | strWalletLoadError = strprintf( | ||
| 1531 | "SQLite wallet export could not be read while handling a '%s' record", | ||
| 1532 | visitor.strLastType.c_str()); | ||
| 1533 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1534 | return false; | ||
| 1535 | } | ||
| 1536 | |||
| 1537 | if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode, | ||
| 1538 | visitor.fHaveStoredHDCoinType)) | ||
| 1539 | { | ||
| 1540 | if (strWalletLoadError.empty()) | ||
| 1541 | strWalletLoadError = "SQLite wallet metadata is not supported by this build"; | ||
| 1542 | return false; | ||
| 1543 | } | ||
| 1544 | |||
| 1545 | return FinishLoadedWallet(visitor.vchDefaultKey, false); | ||
| 1546 | } | ||
| 1547 | |||
| 1548 | // Open a SQLite wallet read-write and keep it open as the active runtime | ||
| 1549 | // backend, then load its records into memory the same way LoadWalletFromSQLite | ||
| 1550 | // does. Unlike the read-only staging path, this leaves pWalletSQLiteRuntime set | ||
| 1551 | // so every later CWalletDB read/write/erase routes to this file for the life of | ||
| 1552 | // the process. The Berkeley DB wallet.dat is never opened. | ||
| 1553 | bool LoadWalletFromSQLiteRuntime(const string& strPath) | ||
| 1554 | { | ||
| 1555 | strWalletLoadError.clear(); | ||
| 1556 | |||
| 1557 | if (!FileExists(strPath.c_str())) | ||
| 1558 | { | ||
| 1559 | strWalletLoadError = strprintf("SQLite wallet does not exist: %s", | ||
| 1560 | strPath.c_str()); | ||
| 1561 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1562 | return false; | ||
| 1563 | } | ||
| 1564 | |||
| 1565 | string strError; | ||
| 1566 | if (!WalletSQLiteRuntimeOpen(strPath, strError)) | ||
| 1567 | { | ||
| 1568 | strWalletLoadError = strError; | ||
| 1569 | printf("LoadWallet: cannot open SQLite wallet backend: %s\n", | ||
| 1570 | strError.c_str()); | ||
| 1571 | return false; | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | CSQLiteWalletRuntimeLoadVisitor visitor; | ||
| 1575 | try | ||
| 1576 | { | ||
| 1577 | CRITICAL_BLOCK(cs_mapKeys) | ||
| 1578 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 1579 | { | ||
| 1580 | if (!pWalletSQLiteRuntime->ScanRecords(visitor, strError)) | ||
| 1581 | { | ||
| 1582 | WalletSQLiteRuntimeClose(); | ||
| 1583 | if (strWalletLoadError.empty()) | ||
| 1584 | strWalletLoadError = strError; | ||
| 1585 | printf("LoadWallet: SQLite wallet backend could not be read. " | ||
| 1586 | "Failed while handling a '%s' record: %s\n", | ||
| 1587 | visitor.strLastType.c_str(), strError.c_str()); | ||
| 1588 | return false; | ||
| 1589 | } | ||
| 1590 | } | ||
| 1591 | } | ||
| 1592 | catch (const std::exception& e) | ||
| 1593 | { | ||
| 1594 | WalletSQLiteRuntimeClose(); | ||
| 1595 | strWalletLoadError = strprintf( | ||
| 1596 | "SQLite wallet backend could not be read while handling a '%s' record: %s", | ||
| 1597 | visitor.strLastType.c_str(), e.what()); | ||
| 1598 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1599 | return false; | ||
| 1600 | } | ||
| 1601 | catch (...) | ||
| 1602 | { | ||
| 1603 | WalletSQLiteRuntimeClose(); | ||
| 1604 | strWalletLoadError = strprintf( | ||
| 1605 | "SQLite wallet backend could not be read while handling a '%s' record", | ||
| 1606 | visitor.strLastType.c_str()); | ||
| 1607 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1608 | return false; | ||
| 1609 | } | ||
| 1610 | |||
| 1611 | // Refuse an empty or non-wallet SQLite file up front, with a message that | ||
| 1612 | // names the likely cause. FinishLoadedWallet below already rejects a | ||
| 1613 | // missing default key (it is the fMayCreateDefaultKey=false path), but its | ||
| 1614 | // wording -- "no usable default public key" -- reads as corruption when the | ||
| 1615 | // real story is usually "this file was never an exported wallet". A blank | ||
| 1616 | // wallet.sqlite left by a typo or a half-run export should say so. | ||
| 1617 | if (visitor.vchDefaultKey.empty()) | ||
| 1618 | { | ||
| 1619 | WalletSQLiteRuntimeClose(); | ||
| 1620 | strWalletLoadError = | ||
| 1621 | "SQLite wallet has no default key record; it may be empty or not a " | ||
| 1622 | "wallet export. Create it with -walletsqliteexport."; | ||
| 1623 | printf("LoadWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1624 | return false; | ||
| 1625 | } | ||
| 1626 | |||
| 1627 | if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode, | ||
| 1628 | visitor.fHaveStoredHDCoinType)) | ||
| 1629 | { | ||
| 1630 | WalletSQLiteRuntimeClose(); | ||
| 1631 | if (strWalletLoadError.empty()) | ||
| 1632 | strWalletLoadError = "SQLite wallet metadata is not supported by this build"; | ||
| 1633 | return false; | ||
| 1634 | } | ||
| 1635 | |||
| 1636 | // Never fabricate a default key: an export always carries one, and a file | ||
| 1637 | // that does not is refused with the wallet left open to no writes rather | ||
| 1638 | // than quietly minted into a new identity. | ||
| 1639 | if (!FinishLoadedWallet(visitor.vchDefaultKey, false)) | ||
| 1640 | { | ||
| 1641 | WalletSQLiteRuntimeClose(); | ||
| 1642 | return false; | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | return true; | ||
| 1646 | } | ||
| 1647 | |||
| 1648 | // Create a brand-new wallet directly in the SQLite backend. For a fresh install | ||
| 1649 | // where SQLite is the default there is no wallet.dat to export from, so open a | ||
| 1650 | // new wallet.sqlite (Open() creates the file and its schema) and let | ||
| 1651 | // FinishLoadedWallet mint the first default key straight into it -- the same | ||
| 1652 | // call the Berkeley DB create path uses, so a new wallet is identical either | ||
| 1653 | // way. Refuses to touch an existing file, and cleans up on failure so a broken | ||
| 1654 | // half-initialised wallet.sqlite is never left for the next start to load. | ||
| 1655 | bool CreateNewSQLiteWallet(const string& strPath) | ||
| 1656 | { | ||
| 1657 | strWalletLoadError.clear(); | ||
| 1658 | |||
| 1659 | if (FileExists(strPath.c_str())) | ||
| 1660 | { | ||
| 1661 | strWalletLoadError = strprintf("SQLite wallet already exists: %s", | ||
| 1662 | strPath.c_str()); | ||
| 1663 | printf("CreateNewSQLiteWallet: %s\n", strWalletLoadError.c_str()); | ||
| 1664 | return false; | ||
| 1665 | } | ||
| 1666 | |||
| 1667 | string strError; | ||
| 1668 | if (!WalletSQLiteRuntimeOpen(strPath, strError, false)) | ||
| 1669 | { | ||
| 1670 | strWalletLoadError = strError; | ||
| 1671 | printf("CreateNewSQLiteWallet: cannot create SQLite wallet: %s\n", | ||
| 1672 | strError.c_str()); | ||
| 1673 | return false; | ||
| 1674 | } | ||
| 1675 | |||
| 1676 | vector<unsigned char> vchNoDefaultKey; | ||
| 1677 | if (!FinishLoadedWallet(vchNoDefaultKey, true)) | ||
| 1678 | { | ||
| 1679 | WalletSQLiteRuntimeClose(); | ||
| 1680 | remove(strPath.c_str()); | ||
| 1681 | remove((strPath + "-wal").c_str()); | ||
| 1682 | remove((strPath + "-shm").c_str()); | ||
| 1683 | if (strWalletLoadError.empty()) | ||
| 1684 | strWalletLoadError = "could not initialise the new SQLite wallet"; | ||
| 1685 | return false; | ||
| 955 | } | 1686 | } |
| 956 | 1687 | ||
| 957 | return true; | 1688 | return true; |
| @@ -974,21 +1705,99 @@ bool LoadWallet() | |||
| 974 | // this file was written are not spendable from it. Backing up once is not | 1705 | // this file was written are not spendable from it. Backing up once is not |
| 975 | // enough, and that is a property of the wallet format rather than of this | 1706 | // enough, and that is a property of the wallet format rather than of this |
| 976 | // function. | 1707 | // function. |
| 1708 | // Back up the active SQLite wallet. The store is checkpointed so the WAL is | ||
| 1709 | // folded into the main file, then the single self-contained wallet.sqlite is | ||
| 1710 | // copied through a temp file and renamed into place (same temp/old/rename dance | ||
| 1711 | // as the Berkeley DB path, for Windows' non-overwriting rename). No lsn_reset: | ||
| 1712 | // a checkpointed SQLite file needs nothing beside it to open. | ||
| 1713 | static bool BackupWalletSQLite(const string& strDest) | ||
| 1714 | { | ||
| 1715 | string strSrc = GetAppDir() + "/wallet.sqlite"; | ||
| 1716 | if (!FileExists(strSrc.c_str())) | ||
| 1717 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); | ||
| 1718 | |||
| 1719 | string strCheckpointError; | ||
| 1720 | if (!WalletSQLiteRuntimeCheckpoint(strCheckpointError)) | ||
| 1721 | return error("BackupWallet() : checkpoint failed: %s\n", strCheckpointError.c_str()); | ||
| 1722 | |||
| 1723 | string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime()); | ||
| 1724 | for (int i = 1; FileExists(strTemp.c_str()); i++) | ||
| 1725 | strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1726 | string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime()); | ||
| 1727 | for (int i = 1; FileExists(strOld.c_str()); i++) | ||
| 1728 | strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1729 | |||
| 1730 | FILE* pfIn = fopen(strSrc.c_str(), "rb"); | ||
| 1731 | if (!pfIn) | ||
| 1732 | return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); | ||
| 1733 | FILE* pfOut = fopen(strTemp.c_str(), "wb"); | ||
| 1734 | if (!pfOut) | ||
| 1735 | { | ||
| 1736 | fclose(pfIn); | ||
| 1737 | return error("BackupWallet() : cannot write %s\n", strTemp.c_str()); | ||
| 1738 | } | ||
| 1739 | |||
| 1740 | char buf[65536]; | ||
| 1741 | size_t n; | ||
| 1742 | bool fOk = true; | ||
| 1743 | while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0) | ||
| 1744 | if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; } | ||
| 1745 | if (ferror(pfIn)) | ||
| 1746 | fOk = false; | ||
| 1747 | fclose(pfIn); | ||
| 1748 | if (fclose(pfOut) != 0) | ||
| 1749 | fOk = false; | ||
| 1750 | if (!fOk) | ||
| 1751 | { | ||
| 1752 | remove(strTemp.c_str()); // half a wallet is worse than none | ||
| 1753 | return error("BackupWallet() : copy to %s failed\n", strTemp.c_str()); | ||
| 1754 | } | ||
| 1755 | |||
| 1756 | bool fHadOldDest = FileExists(strDest.c_str()); | ||
| 1757 | if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0) | ||
| 1758 | { | ||
| 1759 | remove(strTemp.c_str()); | ||
| 1760 | return error("BackupWallet() : could not move old %s aside\n", strDest.c_str()); | ||
| 1761 | } | ||
| 1762 | if (rename(strTemp.c_str(), strDest.c_str()) != 0) | ||
| 1763 | { | ||
| 1764 | remove(strTemp.c_str()); | ||
| 1765 | if (fHadOldDest) | ||
| 1766 | rename(strOld.c_str(), strDest.c_str()); | ||
| 1767 | return error("BackupWallet() : could not install %s\n", strDest.c_str()); | ||
| 1768 | } | ||
| 1769 | if (fHadOldDest) | ||
| 1770 | remove(strOld.c_str()); | ||
| 1771 | return true; | ||
| 1772 | } | ||
| 1773 | |||
| 977 | bool BackupWallet(const string& strDest) | 1774 | bool BackupWallet(const string& strDest) |
| 978 | { | 1775 | { |
| 979 | if (strDest.empty()) | 1776 | if (strDest.empty()) |
| 980 | return error("BackupWallet() : no destination given\n"); | 1777 | return error("BackupWallet() : no destination given\n"); |
| 981 | 1778 | ||
| 1779 | // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat. | ||
| 1780 | if (WalletSQLiteRuntimeActive()) | ||
| 1781 | return BackupWalletSQLite(strDest); | ||
| 1782 | |||
| 982 | string strSrc = GetAppDir() + "/wallet.dat"; | 1783 | string strSrc = GetAppDir() + "/wallet.dat"; |
| 983 | if (!FileExists(strSrc.c_str())) | 1784 | if (!FileExists(strSrc.c_str())) |
| 984 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); | 1785 | return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); |
| 985 | 1786 | ||
| 1787 | string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime()); | ||
| 1788 | for (int i = 1; FileExists(strTemp.c_str()); i++) | ||
| 1789 | strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1790 | string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime()); | ||
| 1791 | for (int i = 1; FileExists(strOld.c_str()); i++) | ||
| 1792 | strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i); | ||
| 1793 | |||
| 986 | CRITICAL_BLOCK(cs_db) | 1794 | CRITICAL_BLOCK(cs_db) |
| 987 | { | 1795 | { |
| 988 | // Committing first, so the copy is not missing the newest records. | 1796 | // Committing first, so the copy is not missing the newest records. |
| 989 | // Both calls can throw, and a failed backup must not take the node | 1797 | // Both calls can throw, and a failed backup must not take the node |
| 990 | // down with it. | 1798 | // down with it. |
| 991 | try { dbenv.txn_checkpoint(0, 0, 0); } | 1799 | DbEnv& env = GetDbEnv(); |
| 1800 | try { env.txn_checkpoint(0, 0, 0); } | ||
| 992 | catch (const std::exception& e) | 1801 | catch (const std::exception& e) |
| 993 | { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); } | 1802 | { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); } |
| 994 | 1803 | ||
| @@ -997,11 +1806,11 @@ bool BackupWallet(const string& strDest) | |||
| 997 | FILE* pfIn = fopen(strSrc.c_str(), "rb"); | 1806 | FILE* pfIn = fopen(strSrc.c_str(), "rb"); |
| 998 | if (!pfIn) | 1807 | if (!pfIn) |
| 999 | return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); | 1808 | return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); |
| 1000 | FILE* pfOut = fopen(strDest.c_str(), "wb"); | 1809 | FILE* pfOut = fopen(strTemp.c_str(), "wb"); |
| 1001 | if (!pfOut) | 1810 | if (!pfOut) |
| 1002 | { | 1811 | { |
| 1003 | fclose(pfIn); | 1812 | fclose(pfIn); |
| 1004 | return error("BackupWallet() : cannot write %s\n", strDest.c_str()); | 1813 | return error("BackupWallet() : cannot write %s\n", strTemp.c_str()); |
| 1005 | } | 1814 | } |
| 1006 | 1815 | ||
| 1007 | char buf[65536]; | 1816 | char buf[65536]; |
| @@ -1018,18 +1827,39 @@ bool BackupWallet(const string& strDest) | |||
| 1018 | 1827 | ||
| 1019 | if (!fOk) | 1828 | if (!fOk) |
| 1020 | { | 1829 | { |
| 1021 | remove(strDest.c_str()); // half a wallet is worse than none | 1830 | remove(strTemp.c_str()); // half a wallet is worse than none |
| 1022 | return error("BackupWallet() : copy to %s failed\n", strDest.c_str()); | 1831 | return error("BackupWallet() : copy to %s failed\n", strTemp.c_str()); |
| 1023 | } | 1832 | } |
| 1024 | 1833 | ||
| 1025 | // Without this the copy is welded to this node's database/ dir. | 1834 | // Without this the copy is welded to this node's database/ dir. |
| 1026 | int ret = dbenv.lsn_reset(strDest.c_str(), 0); | 1835 | int ret = env.lsn_reset(strTemp.c_str(), 0); |
| 1027 | if (ret != 0) | 1836 | if (ret != 0) |
| 1028 | printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " | 1837 | printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " |
| 1029 | "may only open beside this node's database/ directory\n", ret); | 1838 | "may only open beside this node's database/ directory\n", ret); |
| 1839 | |||
| 1840 | bool fHadOldDest = FileExists(strDest.c_str()); | ||
| 1841 | if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0) | ||
| 1842 | { | ||
| 1843 | remove(strTemp.c_str()); | ||
| 1844 | return error("BackupWallet() : could not move old %s aside\n", strDest.c_str()); | ||
| 1845 | } | ||
| 1846 | if (rename(strTemp.c_str(), strDest.c_str()) != 0) | ||
| 1847 | { | ||
| 1848 | remove(strTemp.c_str()); | ||
| 1849 | if (fHadOldDest) | ||
| 1850 | rename(strOld.c_str(), strDest.c_str()); | ||
| 1851 | return error("BackupWallet() : could not install %s\n", strDest.c_str()); | ||
| 1852 | } | ||
| 1853 | if (fHadOldDest) | ||
| 1854 | remove(strOld.c_str()); | ||
| 1030 | } | 1855 | } |
| 1031 | catch (const std::exception& e) | 1856 | catch (const std::exception& e) |
| 1032 | { return error("BackupWallet() : %s\n", e.what()); } | 1857 | { |
| 1858 | remove(strTemp.c_str()); | ||
| 1859 | if (FileExists(strOld.c_str()) && !FileExists(strDest.c_str())) | ||
| 1860 | rename(strOld.c_str(), strDest.c_str()); | ||
| 1861 | return error("BackupWallet() : %s\n", e.what()); | ||
| 1862 | } | ||
| 1033 | } | 1863 | } |
| 1034 | 1864 | ||
| 1035 | // Recorded here rather than by whoever called, so that a backup taken with | 1865 | // Recorded here rather than by whoever called, so that a backup taken with |
| @@ -1307,73 +2137,71 @@ public: | |||
| 1307 | { | 2137 | { |
| 1308 | return Write(make_pair(string("pool"), nIndex), vchPubKey); | 2138 | return Write(make_pair(string("pool"), nIndex), vchPubKey); |
| 1309 | } | 2139 | } |
| 2140 | }; | ||
| 2141 | |||
| 2142 | static bool IsWalletEncryptionPrivateRecord(const string& strType) | ||
| 2143 | { | ||
| 2144 | return strType == "key" || | ||
| 2145 | strType == "pool" || | ||
| 2146 | strType == "hdmaster" || | ||
| 2147 | strType == "hdchaincode" || | ||
| 2148 | strType == "mkey" || | ||
| 2149 | strType == "ckey" || | ||
| 2150 | strType == "cryptedhdmaster" || | ||
| 2151 | strType == "cryptedhdchaincode" || | ||
| 2152 | strType == "walletminversion"; | ||
| 2153 | } | ||
| 2154 | |||
| 2155 | class CWalletPublicRecordCopyVisitor : public CWalletRecordVisitor | ||
| 2156 | { | ||
| 2157 | public: | ||
| 2158 | CWalletRewriteDB& dbTo; | ||
| 2159 | int& nCopied; | ||
| 1310 | 2160 | ||
| 1311 | bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet) | 2161 | CWalletPublicRecordCopyVisitor(CWalletRewriteDB& dbToIn, int& nCopiedIn) |
| 2162 | : dbTo(dbToIn), nCopied(nCopiedIn) { } | ||
| 2163 | |||
| 2164 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 2165 | const CDataStream& ssValue, | ||
| 2166 | string& strErrorRet) | ||
| 1312 | { | 2167 | { |
| 1313 | nCopiedRet = 0; | 2168 | string strType; |
| 1314 | Dbc* pcursor = GetCursor(); | 2169 | try |
| 1315 | if (!pcursor) | ||
| 1316 | { | 2170 | { |
| 1317 | strErrorRet = "could not open a wallet cursor"; | 2171 | CDataStream ssType = ssKey; |
| 1318 | return false; | 2172 | ssType >> strType; |
| 1319 | } | 2173 | } |
| 1320 | 2174 | catch (...) | |
| 1321 | unsigned int fFlags = DB_NEXT; | ||
| 1322 | loop | ||
| 1323 | { | 2175 | { |
| 1324 | CDataStream ssKey; | 2176 | strErrorRet = "wallet.dat has a record whose key type cannot be read"; |
| 1325 | CDataStream ssValue; | 2177 | return false; |
| 1326 | int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags); | 2178 | } |
| 1327 | if (ret == DB_NOTFOUND) | ||
| 1328 | break; | ||
| 1329 | if (ret != 0) | ||
| 1330 | { | ||
| 1331 | pcursor->close(); | ||
| 1332 | strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); | ||
| 1333 | return false; | ||
| 1334 | } | ||
| 1335 | |||
| 1336 | string strType; | ||
| 1337 | try | ||
| 1338 | { | ||
| 1339 | CDataStream ssType = ssKey; | ||
| 1340 | ssType >> strType; | ||
| 1341 | } | ||
| 1342 | catch (...) | ||
| 1343 | { | ||
| 1344 | pcursor->close(); | ||
| 1345 | strErrorRet = "wallet.dat has a record whose key type cannot be read"; | ||
| 1346 | return false; | ||
| 1347 | } | ||
| 1348 | 2179 | ||
| 1349 | if (strType == "key" || | 2180 | if (IsWalletEncryptionPrivateRecord(strType)) |
| 1350 | strType == "pool" || | 2181 | return true; |
| 1351 | strType == "hdmaster" || | ||
| 1352 | strType == "hdchaincode" || | ||
| 1353 | strType == "mkey" || | ||
| 1354 | strType == "ckey" || | ||
| 1355 | strType == "cryptedhdmaster" || | ||
| 1356 | strType == "cryptedhdchaincode" || | ||
| 1357 | strType == "walletminversion") | ||
| 1358 | continue; | ||
| 1359 | 2182 | ||
| 1360 | if (!dbTo.WriteRaw(ssKey, ssValue)) | 2183 | if (!dbTo.WriteRaw(ssKey, ssValue)) |
| 1361 | { | 2184 | { |
| 1362 | pcursor->close(); | 2185 | strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); |
| 1363 | strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); | 2186 | return false; |
| 1364 | return false; | ||
| 1365 | } | ||
| 1366 | nCopiedRet++; | ||
| 1367 | } | 2187 | } |
| 1368 | 2188 | nCopied++; | |
| 1369 | pcursor->close(); | ||
| 1370 | return true; | 2189 | return true; |
| 1371 | } | 2190 | } |
| 1372 | }; | 2191 | }; |
| 1373 | 2192 | ||
| 1374 | static string WalletEncryptBackupPath() | 2193 | static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo, |
| 2194 | int& nCopiedRet, | ||
| 2195 | string& strErrorRet) | ||
| 1375 | { | 2196 | { |
| 1376 | string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; | 2197 | nCopiedRet = 0; |
| 2198 | CWalletPublicRecordCopyVisitor visitor(dbTo, nCopiedRet); | ||
| 2199 | return ScanWalletRecords(visitor, strErrorRet); | ||
| 2200 | } | ||
| 2201 | |||
| 2202 | static string WalletEncryptBackupPathFor(const char* pszBaseName) | ||
| 2203 | { | ||
| 2204 | string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt."; | ||
| 1377 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; | 2205 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; |
| 1378 | for (int i = 1; FileExists(strPath.c_str()); i++) | 2206 | for (int i = 1; FileExists(strPath.c_str()); i++) |
| 1379 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; | 2207 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; |
| @@ -1403,6 +2231,192 @@ static void SecureClearBytes(vector<unsigned char>& vch) | |||
| 1403 | vch.clear(); | 2231 | vch.clear(); |
| 1404 | } | 2232 | } |
| 1405 | 2233 | ||
| 2234 | // The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list, | ||
| 2235 | // same byte-for-byte copy, but the destination is a CWalletDBSQLite record | ||
| 2236 | // store rather than a Berkeley DB file. | ||
| 2237 | class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor | ||
| 2238 | { | ||
| 2239 | public: | ||
| 2240 | CWalletDBSQLite& dbTo; | ||
| 2241 | int& nCopied; | ||
| 2242 | |||
| 2243 | CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn) | ||
| 2244 | : dbTo(dbToIn), nCopied(nCopiedIn) { } | ||
| 2245 | |||
| 2246 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 2247 | const CDataStream& ssValue, | ||
| 2248 | string& strErrorRet) | ||
| 2249 | { | ||
| 2250 | string strType; | ||
| 2251 | try | ||
| 2252 | { | ||
| 2253 | CDataStream ssType = ssKey; | ||
| 2254 | ssType >> strType; | ||
| 2255 | } | ||
| 2256 | catch (...) | ||
| 2257 | { | ||
| 2258 | strErrorRet = "SQLite wallet has a record whose key type cannot be read"; | ||
| 2259 | return false; | ||
| 2260 | } | ||
| 2261 | |||
| 2262 | if (IsWalletEncryptionPrivateRecord(strType)) | ||
| 2263 | return true; | ||
| 2264 | |||
| 2265 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 2266 | vector<unsigned char> vchValue(ssValue.begin(), ssValue.end()); | ||
| 2267 | bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true); | ||
| 2268 | if (!vchKey.empty()) | ||
| 2269 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 2270 | if (!vchValue.empty()) | ||
| 2271 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 2272 | if (!fOk) | ||
| 2273 | { | ||
| 2274 | strErrorRet = strprintf("could not copy wallet record '%s': %s", | ||
| 2275 | strType.c_str(), strErrorRet.c_str()); | ||
| 2276 | return false; | ||
| 2277 | } | ||
| 2278 | nCopied++; | ||
| 2279 | return true; | ||
| 2280 | } | ||
| 2281 | }; | ||
| 2282 | |||
| 2283 | // Persist an encrypted wallet under the SQLite backend. Everything the caller | ||
| 2284 | // computed -- the master key, the re-encrypted private keys, the fresh crypted | ||
| 2285 | // key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built | ||
| 2286 | // in one transaction, which is then swapped in for the plaintext original. | ||
| 2287 | // | ||
| 2288 | // A fresh file rather than an in-place rewrite for two reasons: it keeps the | ||
| 2289 | // whole change atomic (a crash before the rename leaves the old wallet whole), | ||
| 2290 | // and it guarantees no plaintext key survives in a freed page the way an | ||
| 2291 | // in-place DELETE would leave behind until a VACUUM. | ||
| 2292 | static bool EncryptWalletPersistSQLite( | ||
| 2293 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vCryptedKeys, | ||
| 2294 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vNewPoolKeys, | ||
| 2295 | const vector<pair<int64, vector<unsigned char> > >& vNewPool, | ||
| 2296 | const CWalletMasterKey& kMasterKey, | ||
| 2297 | const vector<unsigned char>& vchCryptedHDMasterNew, | ||
| 2298 | const vector<unsigned char>& vchCryptedHDChainCodeNew, | ||
| 2299 | unsigned int nHDNextNew, | ||
| 2300 | int& nCopiedRet, | ||
| 2301 | string& strBackupRet, | ||
| 2302 | string& strErrorRet) | ||
| 2303 | { | ||
| 2304 | nCopiedRet = 0; | ||
| 2305 | if (!pWalletSQLiteRuntime) | ||
| 2306 | { | ||
| 2307 | strErrorRet = "SQLite wallet backend is not active"; | ||
| 2308 | return false; | ||
| 2309 | } | ||
| 2310 | |||
| 2311 | string strWalletPath = GetAppDir() + "/wallet.sqlite"; | ||
| 2312 | string strTempPath = GetAppDir() + "/" + | ||
| 2313 | strprintf("wallet.sqlite.encrypting.%lld", GetTime()); | ||
| 2314 | remove(strTempPath.c_str()); | ||
| 2315 | remove((strTempPath + "-wal").c_str()); | ||
| 2316 | remove((strTempPath + "-shm").c_str()); | ||
| 2317 | |||
| 2318 | { | ||
| 2319 | CWalletDBSQLite dbTmp; | ||
| 2320 | if (!dbTmp.Open(strTempPath, strErrorRet)) | ||
| 2321 | { | ||
| 2322 | remove(strTempPath.c_str()); | ||
| 2323 | return false; | ||
| 2324 | } | ||
| 2325 | if (!dbTmp.BeginTransaction(strErrorRet)) | ||
| 2326 | { | ||
| 2327 | dbTmp.Close(); | ||
| 2328 | remove(strTempPath.c_str()); | ||
| 2329 | return false; | ||
| 2330 | } | ||
| 2331 | |||
| 2332 | // The public records come from the live SQLite store -- under this | ||
| 2333 | // backend ScanWalletRecords() has no Berkeley DB behind it to read. | ||
| 2334 | CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet); | ||
| 2335 | bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); | ||
| 2336 | |||
| 2337 | // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB | ||
| 2338 | // path (CWalletRewriteDB) writes it, so a wallet encrypted under either | ||
| 2339 | // backend is the same set of records. ckey uses INSERT-only to match | ||
| 2340 | // WriteCryptedKey; the rest overwrite. | ||
| 2341 | fOk = fOk && | ||
| 2342 | dbTmp.WriteTypedRecord(string("walletminversion"), | ||
| 2343 | (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) && | ||
| 2344 | dbTmp.WriteTypedRecord(make_pair(string("mkey"), | ||
| 2345 | (unsigned int)(nWalletMasterKeyMaxID + 1)), | ||
| 2346 | kMasterKey, strErrorRet, true); | ||
| 2347 | for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++) | ||
| 2348 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first), | ||
| 2349 | vCryptedKeys[i].second, strErrorRet, false); | ||
| 2350 | for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++) | ||
| 2351 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first), | ||
| 2352 | vNewPoolKeys[i].second, strErrorRet, false); | ||
| 2353 | if (fOk && !vchCryptedHDMasterNew.empty()) | ||
| 2354 | fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"), | ||
| 2355 | vchCryptedHDMasterNew, strErrorRet, true) && | ||
| 2356 | dbTmp.WriteTypedRecord(string("cryptedhdchaincode"), | ||
| 2357 | vchCryptedHDChainCodeNew, strErrorRet, true); | ||
| 2358 | if (fOk) | ||
| 2359 | fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true); | ||
| 2360 | for (size_t i = 0; fOk && i < vNewPool.size(); i++) | ||
| 2361 | fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first), | ||
| 2362 | vNewPool[i].second, strErrorRet, true); | ||
| 2363 | |||
| 2364 | if (!fOk) | ||
| 2365 | { | ||
| 2366 | string strIgnore; | ||
| 2367 | dbTmp.RollbackTransaction(strIgnore); | ||
| 2368 | dbTmp.Close(); | ||
| 2369 | remove(strTempPath.c_str()); | ||
| 2370 | remove((strTempPath + "-wal").c_str()); | ||
| 2371 | remove((strTempPath + "-shm").c_str()); | ||
| 2372 | return false; | ||
| 2373 | } | ||
| 2374 | |||
| 2375 | if (!dbTmp.CommitTransaction(strErrorRet)) | ||
| 2376 | { | ||
| 2377 | dbTmp.Close(); | ||
| 2378 | remove(strTempPath.c_str()); | ||
| 2379 | remove((strTempPath + "-wal").c_str()); | ||
| 2380 | remove((strTempPath + "-shm").c_str()); | ||
| 2381 | return false; | ||
| 2382 | } | ||
| 2383 | string strCheckpointError; | ||
| 2384 | dbTmp.Checkpoint(strCheckpointError); | ||
| 2385 | dbTmp.Close(); | ||
| 2386 | } | ||
| 2387 | |||
| 2388 | // The rebuilt wallet is committed and self-contained. Close the live handle | ||
| 2389 | // so the file can be swapped: Windows will not rename a file that still has | ||
| 2390 | // an open handle, and the node exits right after this. | ||
| 2391 | WalletSQLiteRuntimeClose(); | ||
| 2392 | |||
| 2393 | string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite"); | ||
| 2394 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | ||
| 2395 | { | ||
| 2396 | strErrorRet = strprintf("could not move the original SQLite wallet to %s", | ||
| 2397 | strBackupPath.c_str()); | ||
| 2398 | remove(strTempPath.c_str()); | ||
| 2399 | return false; | ||
| 2400 | } | ||
| 2401 | // The moved-aside wallet owns any leftover write-ahead log or shared-memory | ||
| 2402 | // sidecar; drop stale ones so they cannot shadow the new wallet. | ||
| 2403 | remove((strWalletPath + "-wal").c_str()); | ||
| 2404 | remove((strWalletPath + "-shm").c_str()); | ||
| 2405 | |||
| 2406 | if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) | ||
| 2407 | { | ||
| 2408 | rename(strBackupPath.c_str(), strWalletPath.c_str()); | ||
| 2409 | strErrorRet = "could not install the encrypted SQLite wallet; the original was restored"; | ||
| 2410 | remove(strTempPath.c_str()); | ||
| 2411 | return false; | ||
| 2412 | } | ||
| 2413 | remove((strTempPath + "-wal").c_str()); | ||
| 2414 | remove((strTempPath + "-shm").c_str()); | ||
| 2415 | |||
| 2416 | strBackupRet = strBackupPath; | ||
| 2417 | return true; | ||
| 2418 | } | ||
| 2419 | |||
| 1406 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) | 2420 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) |
| 1407 | { | 2421 | { |
| 1408 | strBackupRet.clear(); | 2422 | strBackupRet.clear(); |
| @@ -1541,15 +2555,32 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 1541 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); | 2555 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); |
| 1542 | } | 2556 | } |
| 1543 | 2557 | ||
| 2558 | // Every ciphertext is computed; nothing has touched the store yet. Under | ||
| 2559 | // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic | ||
| 2560 | // transaction and swapping it in. The Berkeley DB path below is unchanged. | ||
| 2561 | if (WalletSQLiteRuntimeActive()) | ||
| 2562 | { | ||
| 2563 | int nCopiedSQLite = 0; | ||
| 2564 | if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool, | ||
| 2565 | kMasterKey, vchCryptedHDMasterNew, | ||
| 2566 | vchCryptedHDChainCodeNew, nHDNextNew, | ||
| 2567 | nCopiedSQLite, strBackupRet, strErrorRet)) | ||
| 2568 | return false; | ||
| 2569 | printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " | ||
| 2570 | "rebuilt %d key-pool entry(s) [SQLite]\n", | ||
| 2571 | nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), | ||
| 2572 | (int)vNewPool.size()); | ||
| 2573 | return true; | ||
| 2574 | } | ||
| 2575 | |||
| 1544 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); | 2576 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); |
| 1545 | string strTempPath = GetAppDir() + "/" + strTempFile; | 2577 | string strTempPath = GetAppDir() + "/" + strTempFile; |
| 1546 | remove(strTempPath.c_str()); | 2578 | remove(strTempPath.c_str()); |
| 1547 | 2579 | ||
| 1548 | int nCopied = 0; | 2580 | int nCopied = 0; |
| 1549 | { | 2581 | { |
| 1550 | CWalletRewriteDB dbSource("wallet.dat", "r"); | ||
| 1551 | CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); | 2582 | CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); |
| 1552 | if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet)) | 2583 | if (!CopyPublicWalletRecordsTo(dbTarget, nCopied, strErrorRet)) |
| 1553 | { | 2584 | { |
| 1554 | remove(strTempPath.c_str()); | 2585 | remove(strTempPath.c_str()); |
| 1555 | return false; | 2586 | return false; |
| @@ -1609,7 +2640,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 1609 | DBFlush(true); | 2640 | DBFlush(true); |
| 1610 | 2641 | ||
| 1611 | string strWalletPath = GetAppDir() + "/wallet.dat"; | 2642 | string strWalletPath = GetAppDir() + "/wallet.dat"; |
| 1612 | string strBackupPath = WalletEncryptBackupPath(); | 2643 | string strBackupPath = WalletEncryptBackupPathFor("wallet.dat"); |
| 1613 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | 2644 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) |
| 1614 | { | 2645 | { |
| 1615 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); | 2646 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); |
| @@ -46,7 +46,7 @@ public: | |||
| 46 | }; | 46 | }; |
| 47 | 47 | ||
| 48 | 48 | ||
| 49 | extern DbEnv dbenv; | 49 | extern DbEnv& GetDbEnv(); |
| 50 | extern void DBFlush(bool fShutdown); | 50 | extern void DBFlush(bool fShutdown); |
| 51 | // Close the environment and delete its logs. Only the encryption command calls | 51 | // Close the environment and delete its logs. Only the encryption command calls |
| 52 | // this: the logs hold the plaintext records the old wallet wrote. | 52 | // this: the logs hold the plaintext records the old wallet wrote. |
| @@ -235,7 +235,7 @@ public: | |||
| 235 | if (!pdb) | 235 | if (!pdb) |
| 236 | return false; | 236 | return false; |
| 237 | DbTxn* ptxn = NULL; | 237 | DbTxn* ptxn = NULL; |
| 238 | int ret = dbenv.txn_begin(GetTxn(), &ptxn, 0); | 238 | int ret = GetDbEnv().txn_begin(GetTxn(), &ptxn, 0); |
| 239 | if (!ptxn || ret != 0) | 239 | if (!ptxn || ret != 0) |
| 240 | return false; | 240 | return false; |
| 241 | vTxn.push_back(ptxn); | 241 | vTxn.push_back(ptxn); |
| @@ -333,6 +333,24 @@ private: | |||
| 333 | // Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- | 333 | // Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- |
| 334 | // peer discovery is entirely Nostr/.btf-based now. | 334 | // peer discovery is entirely Nostr/.btf-based now. |
| 335 | 335 | ||
| 336 | bool WalletSQLiteRuntimeActive(); | ||
| 337 | bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist=true); | ||
| 338 | void WalletSQLiteRuntimeClose(); | ||
| 339 | bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey, | ||
| 340 | vector<unsigned char>& vchValueRet); | ||
| 341 | bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey, | ||
| 342 | const vector<unsigned char>& vchValue, | ||
| 343 | bool fOverwrite); | ||
| 344 | bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey); | ||
| 345 | bool WalletSQLiteRuntimeBeginTxn(); | ||
| 346 | bool WalletSQLiteRuntimeCommitTxn(); | ||
| 347 | void WalletSQLiteRuntimeRollbackTxn(); | ||
| 348 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); | ||
| 349 | bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); | ||
| 350 | string ReadWalletBackendMarker(); | ||
| 351 | bool WriteWalletBackendMarker(const string& strBackend); | ||
| 352 | bool CreateNewSQLiteWallet(const string& strPath); | ||
| 353 | |||
| 336 | 354 | ||
| 337 | 355 | ||
| 338 | 356 | ||
| @@ -340,64 +358,141 @@ private: | |||
| 340 | class CWalletDB : public CDB | 358 | class CWalletDB : public CDB |
| 341 | { | 359 | { |
| 342 | public: | 360 | public: |
| 343 | CWalletDB(const char* pszMode="r+", bool fTxn=false) : CDB("wallet.dat", pszMode, fTxn) { } | 361 | CWalletDB(const char* pszMode="r+", bool fTxn=false); |
| 344 | private: | 362 | private: |
| 345 | CWalletDB(const CWalletDB&); | 363 | CWalletDB(const CWalletDB&); |
| 346 | void operator=(const CWalletDB&); | 364 | void operator=(const CWalletDB&); |
| 365 | |||
| 366 | template<typename K, typename T> | ||
| 367 | bool ReadWalletRecord(const K& key, T& value) | ||
| 368 | { | ||
| 369 | if (!WalletSQLiteRuntimeActive()) | ||
| 370 | return Read(key, value); | ||
| 371 | |||
| 372 | CDataStream ssKey(SER_DISK); | ||
| 373 | ssKey.reserve(1000); | ||
| 374 | ssKey << key; | ||
| 375 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 376 | vector<unsigned char> vchValue; | ||
| 377 | |||
| 378 | bool fOk = WalletSQLiteRuntimeReadRecord(vchKey, vchValue); | ||
| 379 | if (!vchKey.empty()) | ||
| 380 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 381 | if (!ssKey.empty()) | ||
| 382 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 383 | if (!fOk) | ||
| 384 | return false; | ||
| 385 | |||
| 386 | CDataStream ssValue(vchValue, SER_DISK); | ||
| 387 | ssValue >> value; | ||
| 388 | bool fReadOk = !ssValue.fail(); | ||
| 389 | if (!vchValue.empty()) | ||
| 390 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 391 | if (!ssValue.empty()) | ||
| 392 | memset(&ssValue[0], 0, ssValue.size()); | ||
| 393 | return fReadOk; | ||
| 394 | } | ||
| 395 | |||
| 396 | template<typename K, typename T> | ||
| 397 | bool WriteWalletRecord(const K& key, const T& value, bool fOverwrite=true) | ||
| 398 | { | ||
| 399 | if (!WalletSQLiteRuntimeActive()) | ||
| 400 | return Write(key, value, fOverwrite); | ||
| 401 | |||
| 402 | CDataStream ssKey(SER_DISK); | ||
| 403 | ssKey.reserve(1000); | ||
| 404 | ssKey << key; | ||
| 405 | CDataStream ssValue(SER_DISK); | ||
| 406 | ssValue.reserve(10000); | ||
| 407 | ssValue << value; | ||
| 408 | |||
| 409 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 410 | vector<unsigned char> vchValue(ssValue.begin(), ssValue.end()); | ||
| 411 | bool fOk = WalletSQLiteRuntimeWriteRecord(vchKey, vchValue, fOverwrite); | ||
| 412 | |||
| 413 | if (!vchKey.empty()) | ||
| 414 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 415 | if (!vchValue.empty()) | ||
| 416 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 417 | if (!ssKey.empty()) | ||
| 418 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 419 | if (!ssValue.empty()) | ||
| 420 | memset(&ssValue[0], 0, ssValue.size()); | ||
| 421 | return fOk; | ||
| 422 | } | ||
| 423 | |||
| 424 | template<typename K> | ||
| 425 | bool EraseWalletRecord(const K& key) | ||
| 426 | { | ||
| 427 | if (!WalletSQLiteRuntimeActive()) | ||
| 428 | return Erase(key); | ||
| 429 | |||
| 430 | CDataStream ssKey(SER_DISK); | ||
| 431 | ssKey.reserve(1000); | ||
| 432 | ssKey << key; | ||
| 433 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 434 | bool fOk = WalletSQLiteRuntimeEraseRecord(vchKey); | ||
| 435 | |||
| 436 | if (!vchKey.empty()) | ||
| 437 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 438 | if (!ssKey.empty()) | ||
| 439 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 440 | return fOk; | ||
| 441 | } | ||
| 347 | public: | 442 | public: |
| 348 | bool ReadName(const string& strAddress, string& strName) | 443 | bool ReadName(const string& strAddress, string& strName) |
| 349 | { | 444 | { |
| 350 | strName = ""; | 445 | strName = ""; |
| 351 | return Read(make_pair(string("name"), strAddress), strName); | 446 | return ReadWalletRecord(make_pair(string("name"), strAddress), strName); |
| 352 | } | 447 | } |
| 353 | 448 | ||
| 354 | bool WriteName(const string& strAddress, const string& strName) | 449 | bool WriteName(const string& strAddress, const string& strName) |
| 355 | { | 450 | { |
| 356 | mapAddressBook[strAddress] = strName; | 451 | mapAddressBook[strAddress] = strName; |
| 357 | return Write(make_pair(string("name"), strAddress), strName); | 452 | return WriteWalletRecord(make_pair(string("name"), strAddress), strName); |
| 358 | } | 453 | } |
| 359 | 454 | ||
| 360 | bool EraseName(const string& strAddress) | 455 | bool EraseName(const string& strAddress) |
| 361 | { | 456 | { |
| 362 | mapAddressBook.erase(strAddress); | 457 | mapAddressBook.erase(strAddress); |
| 363 | return Erase(make_pair(string("name"), strAddress)); | 458 | return EraseWalletRecord(make_pair(string("name"), strAddress)); |
| 364 | } | 459 | } |
| 365 | 460 | ||
| 366 | bool ReadTx(uint256 hash, CWalletTx& wtx) | 461 | bool ReadTx(uint256 hash, CWalletTx& wtx) |
| 367 | { | 462 | { |
| 368 | return Read(make_pair(string("tx"), hash), wtx); | 463 | return ReadWalletRecord(make_pair(string("tx"), hash), wtx); |
| 369 | } | 464 | } |
| 370 | 465 | ||
| 371 | bool WriteTx(uint256 hash, const CWalletTx& wtx) | 466 | bool WriteTx(uint256 hash, const CWalletTx& wtx) |
| 372 | { | 467 | { |
| 373 | return Write(make_pair(string("tx"), hash), wtx); | 468 | return WriteWalletRecord(make_pair(string("tx"), hash), wtx); |
| 374 | } | 469 | } |
| 375 | 470 | ||
| 376 | bool EraseTx(uint256 hash) | 471 | bool EraseTx(uint256 hash) |
| 377 | { | 472 | { |
| 378 | return Erase(make_pair(string("tx"), hash)); | 473 | return EraseWalletRecord(make_pair(string("tx"), hash)); |
| 379 | } | 474 | } |
| 380 | 475 | ||
| 381 | bool ReadKey(const vector<unsigned char>& vchPubKey, CPrivKey& vchPrivKey) | 476 | bool ReadKey(const vector<unsigned char>& vchPubKey, CPrivKey& vchPrivKey) |
| 382 | { | 477 | { |
| 383 | vchPrivKey.clear(); | 478 | vchPrivKey.clear(); |
| 384 | return Read(make_pair(string("key"), vchPubKey), vchPrivKey); | 479 | return ReadWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey); |
| 385 | } | 480 | } |
| 386 | 481 | ||
| 387 | bool WriteKey(const vector<unsigned char>& vchPubKey, const CPrivKey& vchPrivKey) | 482 | bool WriteKey(const vector<unsigned char>& vchPubKey, const CPrivKey& vchPrivKey) |
| 388 | { | 483 | { |
| 389 | return Write(make_pair(string("key"), vchPubKey), vchPrivKey, false); | 484 | return WriteWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey, false); |
| 390 | } | 485 | } |
| 391 | 486 | ||
| 392 | bool ReadDefaultKey(vector<unsigned char>& vchPubKey) | 487 | bool ReadDefaultKey(vector<unsigned char>& vchPubKey) |
| 393 | { | 488 | { |
| 394 | vchPubKey.clear(); | 489 | vchPubKey.clear(); |
| 395 | return Read(string("defaultkey"), vchPubKey); | 490 | return ReadWalletRecord(string("defaultkey"), vchPubKey); |
| 396 | } | 491 | } |
| 397 | 492 | ||
| 398 | bool WriteDefaultKey(const vector<unsigned char>& vchPubKey) | 493 | bool WriteDefaultKey(const vector<unsigned char>& vchPubKey) |
| 399 | { | 494 | { |
| 400 | return Write(string("defaultkey"), vchPubKey); | 495 | return WriteWalletRecord(string("defaultkey"), vchPubKey); |
| 401 | } | 496 | } |
| 402 | 497 | ||
| 403 | // --- Deterministic (BIP32) seed --------------------------------------- | 498 | // --- Deterministic (BIP32) seed --------------------------------------- |
| @@ -415,62 +510,75 @@ public: | |||
| 415 | { | 510 | { |
| 416 | vchMasterRet.clear(); | 511 | vchMasterRet.clear(); |
| 417 | vchChainCodeRet.clear(); | 512 | vchChainCodeRet.clear(); |
| 418 | return Read(string("hdmaster"), vchMasterRet) | 513 | return ReadWalletRecord(string("hdmaster"), vchMasterRet) |
| 419 | && Read(string("hdchaincode"), vchChainCodeRet); | 514 | && ReadWalletRecord(string("hdchaincode"), vchChainCodeRet); |
| 420 | } | 515 | } |
| 421 | 516 | ||
| 422 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) | 517 | bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) |
| 423 | { | 518 | { |
| 424 | return Write(string("hdmaster"), vchMaster) | 519 | // hdmaster and hdchaincode are halves of one seed: a wallet with one but |
| 425 | && Write(string("hdchaincode"), vchChainCode); | 520 | // not the other is an incomplete HD wallet (-walletstoragecheck flags |
| 521 | // exactly that). Under the SQLite runtime write them in one transaction | ||
| 522 | // so a failure or crash between the two cannot leave that half-state. | ||
| 523 | // Berkeley DB keeps its existing per-write behaviour -- the bracket is a | ||
| 524 | // no-op there. | ||
| 525 | if (!WalletSQLiteRuntimeBeginTxn()) | ||
| 526 | return false; | ||
| 527 | if (!WriteWalletRecord(string("hdmaster"), vchMaster) || | ||
| 528 | !WriteWalletRecord(string("hdchaincode"), vchChainCode)) | ||
| 529 | { | ||
| 530 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 531 | return false; | ||
| 532 | } | ||
| 533 | return WalletSQLiteRuntimeCommitTxn(); | ||
| 426 | } | 534 | } |
| 427 | 535 | ||
| 428 | bool WriteWalletMinVersion(int nVersion) | 536 | bool WriteWalletMinVersion(int nVersion) |
| 429 | { | 537 | { |
| 430 | return Write(string("walletminversion"), nVersion); | 538 | return WriteWalletRecord(string("walletminversion"), nVersion); |
| 431 | } | 539 | } |
| 432 | 540 | ||
| 433 | bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) | 541 | bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) |
| 434 | { | 542 | { |
| 435 | return Write(make_pair(string("mkey"), nID), kMasterKey, true); | 543 | return WriteWalletRecord(make_pair(string("mkey"), nID), kMasterKey, true); |
| 436 | } | 544 | } |
| 437 | 545 | ||
| 438 | bool WriteCryptedKey(const vector<unsigned char>& vchPubKey, | 546 | bool WriteCryptedKey(const vector<unsigned char>& vchPubKey, |
| 439 | const vector<unsigned char>& vchCryptedSecret) | 547 | const vector<unsigned char>& vchCryptedSecret) |
| 440 | { | 548 | { |
| 441 | return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); | 549 | return WriteWalletRecord(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); |
| 442 | } | 550 | } |
| 443 | 551 | ||
| 444 | bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster, | 552 | bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster, |
| 445 | const vector<unsigned char>& vchCryptedChainCode) | 553 | const vector<unsigned char>& vchCryptedChainCode) |
| 446 | { | 554 | { |
| 447 | return Write(string("cryptedhdmaster"), vchCryptedMaster) | 555 | return WriteWalletRecord(string("cryptedhdmaster"), vchCryptedMaster) |
| 448 | && Write(string("cryptedhdchaincode"), vchCryptedChainCode); | 556 | && WriteWalletRecord(string("cryptedhdchaincode"), vchCryptedChainCode); |
| 449 | } | 557 | } |
| 450 | 558 | ||
| 451 | bool WriteHDNext(unsigned int nNext) | 559 | bool WriteHDNext(unsigned int nNext) |
| 452 | { | 560 | { |
| 453 | return Write(string("hdnext"), nNext); | 561 | return WriteWalletRecord(string("hdnext"), nNext); |
| 454 | } | 562 | } |
| 455 | 563 | ||
| 456 | bool WriteHDSchema(int nSchema) | 564 | bool WriteHDSchema(int nSchema) |
| 457 | { | 565 | { |
| 458 | return Write(string("hdschema"), nSchema); | 566 | return WriteWalletRecord(string("hdschema"), nSchema); |
| 459 | } | 567 | } |
| 460 | 568 | ||
| 461 | bool WriteHDCoinType(unsigned int nCoinType) | 569 | bool WriteHDCoinType(unsigned int nCoinType) |
| 462 | { | 570 | { |
| 463 | return Write(string("hdcointype"), nCoinType); | 571 | return WriteWalletRecord(string("hdcointype"), nCoinType); |
| 464 | } | 572 | } |
| 465 | 573 | ||
| 466 | bool WriteHDReceiveNext(unsigned int nNext) | 574 | bool WriteHDReceiveNext(unsigned int nNext) |
| 467 | { | 575 | { |
| 468 | return Write(string("hdreceivenext"), nNext); | 576 | return WriteWalletRecord(string("hdreceivenext"), nNext); |
| 469 | } | 577 | } |
| 470 | 578 | ||
| 471 | bool WriteHDChangeNext(unsigned int nNext) | 579 | bool WriteHDChangeNext(unsigned int nNext) |
| 472 | { | 580 | { |
| 473 | return Write(string("hdchangenext"), nNext); | 581 | return WriteWalletRecord(string("hdchangenext"), nNext); |
| 474 | } | 582 | } |
| 475 | 583 | ||
| 476 | // Keys generated ahead of time and not yet handed out. The private key of | 584 | // Keys generated ahead of time and not yet handed out. The private key of |
| @@ -478,30 +586,43 @@ public: | |||
| 478 | // record only says that this one is still unspoken for. | 586 | // record only says that this one is still unspoken for. |
| 479 | bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey) | 587 | bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey) |
| 480 | { | 588 | { |
| 481 | return Write(make_pair(string("pool"), nIndex), vchPubKey); | 589 | return WriteWalletRecord(make_pair(string("pool"), nIndex), vchPubKey); |
| 482 | } | 590 | } |
| 483 | 591 | ||
| 484 | bool ErasePool(int64 nIndex) | 592 | bool ErasePool(int64 nIndex) |
| 485 | { | 593 | { |
| 486 | return Erase(make_pair(string("pool"), nIndex)); | 594 | return EraseWalletRecord(make_pair(string("pool"), nIndex)); |
| 487 | } | 595 | } |
| 488 | 596 | ||
| 489 | template<typename T> | 597 | template<typename T> |
| 490 | bool ReadSetting(const string& strKey, T& value) | 598 | bool ReadSetting(const string& strKey, T& value) |
| 491 | { | 599 | { |
| 492 | return Read(make_pair(string("setting"), strKey), value); | 600 | return ReadWalletRecord(make_pair(string("setting"), strKey), value); |
| 493 | } | 601 | } |
| 494 | 602 | ||
| 495 | template<typename T> | 603 | template<typename T> |
| 496 | bool WriteSetting(const string& strKey, const T& value) | 604 | bool WriteSetting(const string& strKey, const T& value) |
| 497 | { | 605 | { |
| 498 | return Write(make_pair(string("setting"), strKey), value); | 606 | return WriteWalletRecord(make_pair(string("setting"), strKey), value); |
| 499 | } | 607 | } |
| 500 | 608 | ||
| 501 | bool LoadWallet(vector<unsigned char>& vchDefaultKeyRet); | 609 | bool LoadWallet(vector<unsigned char>& vchDefaultKeyRet); |
| 502 | }; | 610 | }; |
| 503 | 611 | ||
| 612 | class CWalletRecordVisitor | ||
| 613 | { | ||
| 614 | public: | ||
| 615 | virtual ~CWalletRecordVisitor() { } | ||
| 616 | virtual bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 617 | const CDataStream& ssValue, | ||
| 618 | string& strErrorRet) = 0; | ||
| 619 | }; | ||
| 620 | |||
| 621 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet); | ||
| 622 | |||
| 504 | bool LoadWallet(); | 623 | bool LoadWallet(); |
| 624 | bool LoadWalletFromSQLite(const string& strPath); | ||
| 625 | bool LoadWalletFromSQLiteRuntime(const string& strPath); | ||
| 505 | 626 | ||
| 506 | // Writes a wallet.dat that opens on its own, anywhere -- see the definition in | 627 | // Writes a wallet.dat that opens on its own, anywhere -- see the definition in |
| 507 | // db.cpp for why a plain file copy does not. Still a point-in-time snapshot: | 628 | // db.cpp for why a plain file copy does not. Still a point-in-time snapshot: |
diff --git a/src/gui.cpp b/src/gui.cpp index 8dcb854..ff6059a 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -11,10 +11,16 @@ | |||
| 11 | #include <ctime> | 11 | #include <ctime> |
| 12 | #include <cstdio> | 12 | #include <cstdio> |
| 13 | #include <sstream> | 13 | #include <sstream> |
| 14 | #ifdef _WIN32 | ||
| 15 | #include <shellapi.h> // ShellExecuteA, to open the website in the browser | ||
| 16 | #else | ||
| 17 | #include <unistd.h> | ||
| 18 | #endif | ||
| 14 | #include <iomanip> | 19 | #include <iomanip> |
| 15 | #include <atomic> | 20 | #include <atomic> |
| 16 | #include <mutex> | 21 | #include <mutex> |
| 17 | #include <thread> | 22 | #include <thread> |
| 23 | #include <system_error> | ||
| 18 | #include "bip32.h" | 24 | #include "bip32.h" |
| 19 | #include "walletcmd.h" | 25 | #include "walletcmd.h" |
| 20 | #include "font_roboto.h" | 26 | #include "font_roboto.h" |
| @@ -172,7 +178,11 @@ static bool PathIsInsideDataDir(const std::string& strPath) | |||
| 172 | 178 | ||
| 173 | static void SetDefaultBackupPath() | 179 | static void SetDefaultBackupPath() |
| 174 | { | 180 | { |
| 175 | std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; | 181 | // Match the extension to the live backend: BackupWallet copies wallet.sqlite |
| 182 | // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat" | ||
| 183 | // default name would put SQLite contents in a misleadingly-named file. | ||
| 184 | const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat"; | ||
| 185 | std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt; | ||
| 176 | strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); | 186 | strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); |
| 177 | g_backupPath[sizeof(g_backupPath)-1] = '\0'; | 187 | g_backupPath[sizeof(g_backupPath)-1] = '\0'; |
| 178 | } | 188 | } |
| @@ -910,8 +920,16 @@ static void DrawOptionsDialog() | |||
| 910 | 920 | ||
| 911 | if (nMineMode == MINE_OPERATOR) { | 921 | if (nMineMode == MINE_OPERATOR) { |
| 912 | if (!gPoolServerRunning) { | 922 | if (!gPoolServerRunning) { |
| 913 | gPoolServerRunning = true; | 923 | // Claim the server is up only once it is. The flag used to |
| 914 | _beginthread(ThreadRPCServer, 0, NULL); | 924 | // be set first and the result never looked at, so a refused |
| 925 | // spawn left an operator with a pool that reports itself | ||
| 926 | // running, no RPC server for miners to reach, and nothing | ||
| 927 | // anywhere saying why. The flag also latches: with it set, | ||
| 928 | // this branch is never retried. | ||
| 929 | if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1) | ||
| 930 | printf("Error: _beginthread(ThreadRPCServer) failed -- pool server not started\n"); | ||
| 931 | else | ||
| 932 | gPoolServerRunning = true; | ||
| 915 | } else if (!gPoolRunning) { | 933 | } else if (!gPoolRunning) { |
| 916 | gPoolRunning = true; | 934 | gPoolRunning = true; |
| 917 | } | 935 | } |
| @@ -1124,11 +1142,28 @@ static void DrawRestorePhraseDialog() | |||
| 1124 | } | 1142 | } |
| 1125 | g_restoreDerived.store(0); | 1143 | g_restoreDerived.store(0); |
| 1126 | g_restoreRecovered.store(0); | 1144 | g_restoreRecovered.store(0); |
| 1145 | // Set before the spawn, not after: RestoreThread clears this | ||
| 1146 | // flag itself when it finishes, and a short restore can beat | ||
| 1147 | // us back here and have its `false` overwritten, leaving the | ||
| 1148 | // dialog disabled forever. | ||
| 1127 | g_restoreRunning.store(true); | 1149 | g_restoreRunning.store(true); |
| 1128 | // On its own thread: a restore walks the whole chain once per | 1150 | // On its own thread: a restore walks the whole chain once per |
| 1129 | // batch and would otherwise freeze the window for as long as it | 1151 | // batch and would otherwise freeze the window for as long as it |
| 1130 | // takes. | 1152 | // takes. A refused spawn is one restore that does not happen, |
| 1131 | std::thread(RestoreThread, std::string(g_restoreInput)).detach(); | 1153 | // not a reason to take the window down with an uncaught |
| 1154 | // std::system_error -- and the flag has to come back off, or | ||
| 1155 | // the dialog sits greyed out reporting a search nobody is | ||
| 1156 | // running. | ||
| 1157 | try | ||
| 1158 | { | ||
| 1159 | std::thread(RestoreThread, std::string(g_restoreInput)).detach(); | ||
| 1160 | } | ||
| 1161 | catch (const std::system_error& e) | ||
| 1162 | { | ||
| 1163 | g_restoreRunning.store(false); | ||
| 1164 | std::lock_guard<std::mutex> lock(g_restoreResultMutex); | ||
| 1165 | g_restoreResult = std::string("Could not start the search: ") + e.what(); | ||
| 1166 | } | ||
| 1132 | } | 1167 | } |
| 1133 | } | 1168 | } |
| 1134 | if (fBusy) | 1169 | if (fBusy) |
| @@ -1254,11 +1289,11 @@ static void DrawWalletSafetyDialog() | |||
| 1254 | } | 1289 | } |
| 1255 | ImGui::Text("Phrase-backed spendable balance: %s BTF", | 1290 | ImGui::Text("Phrase-backed spendable balance: %s BTF", |
| 1256 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); | 1291 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); |
| 1257 | ImGui::Text("Wallet.dat-only spendable balance: %s BTF", | 1292 | ImGui::Text("Not phrase-backed spendable balance: %s BTF", |
| 1258 | FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); | 1293 | FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); |
| 1259 | ImGui::Text("Phrase-backed immature mining rewards: %s BTF", | 1294 | ImGui::Text("Phrase-backed immature mining rewards: %s BTF", |
| 1260 | FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); | 1295 | FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); |
| 1261 | ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", | 1296 | ImGui::Text("Not phrase-backed immature mining rewards: %s BTF", |
| 1262 | FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); | 1297 | FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); |
| 1263 | if (!g_recoveryAudit.fDeriveComplete) | 1298 | if (!g_recoveryAudit.fDeriveComplete) |
| 1264 | { | 1299 | { |
| @@ -1272,8 +1307,8 @@ static void DrawWalletSafetyDialog() | |||
| 1272 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | 1307 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); |
| 1273 | ImGui::TextWrapped( | 1308 | ImGui::TextWrapped( |
| 1274 | "Some coins are on keys the phrase does not reproduce. " | 1309 | "Some coins are on keys the phrase does not reproduce. " |
| 1275 | "Keep wallet.dat backups until that balance has been moved to a " | 1310 | "Keep a file backup of this wallet until that balance has been " |
| 1276 | "phrase-backed address."); | 1311 | "moved to a phrase-backed address."); |
| 1277 | ImGui::PopStyleColor(); | 1312 | ImGui::PopStyleColor(); |
| 1278 | } | 1313 | } |
| 1279 | else if (HaveHDSeed() && | 1314 | else if (HaveHDSeed() && |
| @@ -1369,16 +1404,38 @@ static void DrawDiagnosticsDialog() | |||
| 1369 | ImGui::End(); | 1404 | ImGui::End(); |
| 1370 | } | 1405 | } |
| 1371 | 1406 | ||
| 1407 | static void OpenURL(const char* pszUrl) | ||
| 1408 | { | ||
| 1409 | #ifdef _WIN32 | ||
| 1410 | ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL); | ||
| 1411 | #else | ||
| 1412 | std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &"; | ||
| 1413 | int r = system(cmd.c_str()); | ||
| 1414 | (void)r; | ||
| 1415 | #endif | ||
| 1416 | } | ||
| 1417 | |||
| 1372 | static void DrawAboutDialog() | 1418 | static void DrawAboutDialog() |
| 1373 | { | 1419 | { |
| 1374 | if (!g_showAbout) return; | 1420 | if (!g_showAbout) return; |
| 1375 | ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); | 1421 | ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always); |
| 1376 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | 1422 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), |
| 1377 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | 1423 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); |
| 1378 | if (ImGui::Begin("About Bitflash", &g_showAbout, | 1424 | if (ImGui::Begin("About Bitflash", &g_showAbout, |
| 1379 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | 1425 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) |
| 1380 | { | 1426 | { |
| 1381 | ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); | 1427 | ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), |
| 1428 | "Bitflash BTF v" BITFLASH_VERSION_STRING); | ||
| 1429 | ImGui::Spacing(); | ||
| 1430 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f)); | ||
| 1431 | ImGui::TextUnformatted("https://bitflash.network"); | ||
| 1432 | ImGui::PopStyleColor(); | ||
| 1433 | if (ImGui::IsItemHovered()) | ||
| 1434 | { | ||
| 1435 | ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); | ||
| 1436 | if (ImGui::IsMouseClicked(0)) | ||
| 1437 | OpenURL("https://bitflash.network"); | ||
| 1438 | } | ||
| 1382 | ImGui::Spacing(); | 1439 | ImGui::Spacing(); |
| 1383 | ImGui::TextWrapped( | 1440 | ImGui::TextWrapped( |
| 1384 | "CPU-only cryptocurrency. RandomX proof of work. " | 1441 | "CPU-only cryptocurrency. RandomX proof of work. " |
| @@ -1391,6 +1448,184 @@ static void DrawAboutDialog() | |||
| 1391 | ImGui::End(); | 1448 | ImGui::End(); |
| 1392 | } | 1449 | } |
| 1393 | 1450 | ||
| 1451 | // First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the | ||
| 1452 | // button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite | ||
| 1453 | // and records the backend marker (wallet.dat is left untouched, so a restart | ||
| 1454 | // picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB" | ||
| 1455 | // just records that choice so the offer does not return; "Decide later" leaves | ||
| 1456 | // no marker and asks again next start. | ||
| 1457 | static bool g_showBackendWizard = false; | ||
| 1458 | static std::string g_wizardStatus; | ||
| 1459 | static bool g_wizardConverted = false; | ||
| 1460 | static int g_restartArgc = 0; | ||
| 1461 | static char** g_restartArgv = NULL; | ||
| 1462 | |||
| 1463 | #ifdef _WIN32 | ||
| 1464 | // Quote one argument so CommandLineToArgvW parses it back to exactly this | ||
| 1465 | // string: only backslashes that immediately precede a double quote (including | ||
| 1466 | // the closing one) are doubled, so ordinary Windows paths are not mangled. | ||
| 1467 | static std::string QuoteArgWin(const std::string& a) | ||
| 1468 | { | ||
| 1469 | if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos) | ||
| 1470 | return a; | ||
| 1471 | std::string r = "\""; | ||
| 1472 | size_t bs = 0; | ||
| 1473 | for (size_t i = 0; i < a.size(); i++) | ||
| 1474 | { | ||
| 1475 | if (a[i] == '\\') { bs++; continue; } | ||
| 1476 | if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; } | ||
| 1477 | r.append(bs, '\\'); bs = 0; r += a[i]; | ||
| 1478 | } | ||
| 1479 | r.append(bs * 2, '\\'); | ||
| 1480 | r += "\""; | ||
| 1481 | return r; | ||
| 1482 | } | ||
| 1483 | #endif | ||
| 1484 | |||
| 1485 | // Relaunch Bitflash with the same arguments and datadir, then ask this instance | ||
| 1486 | // to exit -- but only once the relaunch has actually started, so a failure never | ||
| 1487 | // leaves the user with no node running. The new instance waits (-restartwait) | ||
| 1488 | // before it opens the network port and the block index, so this one is gone | ||
| 1489 | // first and they never fight over the same files. | ||
| 1490 | static void RestartApplication() | ||
| 1491 | { | ||
| 1492 | bool fLaunched = false; | ||
| 1493 | #ifdef _WIN32 | ||
| 1494 | char exePath[MAX_PATH]; | ||
| 1495 | if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0) | ||
| 1496 | return; | ||
| 1497 | std::string strCmd = QuoteArgWin(exePath); | ||
| 1498 | bool fHasDataDir = false; | ||
| 1499 | for (int i = 1; i < g_restartArgc; i++) | ||
| 1500 | { | ||
| 1501 | std::string a = g_restartArgv[i] ? g_restartArgv[i] : ""; | ||
| 1502 | if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0) | ||
| 1503 | fHasDataDir = true; | ||
| 1504 | strCmd += " " + QuoteArgWin(a); | ||
| 1505 | } | ||
| 1506 | if (!fHasDataDir) | ||
| 1507 | strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir()); | ||
| 1508 | strCmd += " -restartwait"; | ||
| 1509 | |||
| 1510 | std::vector<char> cmdBuf(strCmd.begin(), strCmd.end()); | ||
| 1511 | cmdBuf.push_back('\0'); | ||
| 1512 | STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); | ||
| 1513 | PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); | ||
| 1514 | if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) | ||
| 1515 | { | ||
| 1516 | CloseHandle(pi.hProcess); | ||
| 1517 | CloseHandle(pi.hThread); | ||
| 1518 | fLaunched = true; | ||
| 1519 | } | ||
| 1520 | #else | ||
| 1521 | pid_t pid = fork(); | ||
| 1522 | if (pid == 0) | ||
| 1523 | { | ||
| 1524 | // Child: wait for the parent to release the port and files, then re-exec. | ||
| 1525 | // /proc/self/exe is the reliable absolute path; argv[0] may be relative | ||
| 1526 | // or a bare name that execv() cannot resolve. | ||
| 1527 | sleep(2); | ||
| 1528 | char exePath[4096]; | ||
| 1529 | ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); | ||
| 1530 | if (len > 0) | ||
| 1531 | { | ||
| 1532 | exePath[len] = '\0'; | ||
| 1533 | execv(exePath, g_restartArgv); | ||
| 1534 | } | ||
| 1535 | if (g_restartArgv) | ||
| 1536 | execv(g_restartArgv[0], g_restartArgv); | ||
| 1537 | _exit(127); | ||
| 1538 | } | ||
| 1539 | fLaunched = (pid > 0); | ||
| 1540 | #endif | ||
| 1541 | |||
| 1542 | if (fLaunched) | ||
| 1543 | { | ||
| 1544 | fShutdown = true; | ||
| 1545 | if (glfwGetCurrentContext()) | ||
| 1546 | glfwSetWindowShouldClose(glfwGetCurrentContext(), true); | ||
| 1547 | } | ||
| 1548 | else | ||
| 1549 | g_wizardStatus = "Could not relaunch automatically -- please restart " | ||
| 1550 | "Bitflash yourself to switch to the SQLite wallet."; | ||
| 1551 | } | ||
| 1552 | |||
| 1553 | static void DrawBackendWizardDialog() | ||
| 1554 | { | ||
| 1555 | if (!g_showBackendWizard) return; | ||
| 1556 | ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always); | ||
| 1557 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 1558 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 1559 | if (ImGui::Begin("Wallet Storage", &g_showBackendWizard, | ||
| 1560 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 1561 | { | ||
| 1562 | if (g_wizardConverted) | ||
| 1563 | { | ||
| 1564 | ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite."); | ||
| 1565 | ImGui::TextWrapped("%s", g_wizardStatus.c_str()); | ||
| 1566 | ImGui::Spacing(); | ||
| 1567 | ImGui::TextWrapped( | ||
| 1568 | "The switch takes effect when Bitflash restarts. Until then it is " | ||
| 1569 | "still running on the old Berkeley DB wallet. Your wallet.dat is " | ||
| 1570 | "kept untouched as a fallback."); | ||
| 1571 | ImGui::Spacing(); | ||
| 1572 | if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f))) | ||
| 1573 | RestartApplication(); | ||
| 1574 | ImGui::SameLine(); | ||
| 1575 | if (ImGui::Button("Later", ImVec2(120.0f, 0.0f))) | ||
| 1576 | g_showBackendWizard = false; | ||
| 1577 | ImGui::Spacing(); | ||
| 1578 | ImGui::TextDisabled( | ||
| 1579 | "If you keep mining before restarting, new coins go to the old " | ||
| 1580 | "wallet until you switch."); | ||
| 1581 | } | ||
| 1582 | else | ||
| 1583 | { | ||
| 1584 | ImGui::TextWrapped( | ||
| 1585 | "Bitflash can store your wallet in a new SQLite format: a single " | ||
| 1586 | "file that is easier to back up and does not depend on the " | ||
| 1587 | "database/ directory beside it."); | ||
| 1588 | ImGui::Spacing(); | ||
| 1589 | ImGui::TextWrapped( | ||
| 1590 | "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it " | ||
| 1591 | "is -- nothing is deleted, and you can go back to it at any time. " | ||
| 1592 | "The change takes effect after you restart."); | ||
| 1593 | ImGui::Spacing(); | ||
| 1594 | if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f))) | ||
| 1595 | { | ||
| 1596 | int nCopied = 0; | ||
| 1597 | std::string strError; | ||
| 1598 | if (DoConvertWalletToSQLite(nCopied, strError)) | ||
| 1599 | { | ||
| 1600 | g_wizardStatus = | ||
| 1601 | strprintf("%d record(s) copied to wallet.sqlite.", nCopied); | ||
| 1602 | g_wizardConverted = true; | ||
| 1603 | } | ||
| 1604 | else | ||
| 1605 | g_wizardStatus = "Conversion failed: " + strError; | ||
| 1606 | } | ||
| 1607 | ImGui::SameLine(); | ||
| 1608 | if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f))) | ||
| 1609 | { | ||
| 1610 | WriteWalletBackendMarker("bdb"); | ||
| 1611 | g_showBackendWizard = false; | ||
| 1612 | } | ||
| 1613 | ImGui::SameLine(); | ||
| 1614 | if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f))) | ||
| 1615 | g_showBackendWizard = false; | ||
| 1616 | |||
| 1617 | if (!g_wizardStatus.empty()) | ||
| 1618 | { | ||
| 1619 | ImGui::Spacing(); | ||
| 1620 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f)); | ||
| 1621 | ImGui::TextWrapped("%s", g_wizardStatus.c_str()); | ||
| 1622 | ImGui::PopStyleColor(); | ||
| 1623 | } | ||
| 1624 | } | ||
| 1625 | } | ||
| 1626 | ImGui::End(); | ||
| 1627 | } | ||
| 1628 | |||
| 1394 | // --------------------------------------------------------------------------- | 1629 | // --------------------------------------------------------------------------- |
| 1395 | // Entry point | 1630 | // Entry point |
| 1396 | // --------------------------------------------------------------------------- | 1631 | // --------------------------------------------------------------------------- |
| @@ -1457,6 +1692,21 @@ int RunGUI(int argc, char* argv[]) | |||
| 1457 | g_mineRadio = nMineMode; | 1692 | g_mineRadio = nMineMode; |
| 1458 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); | 1693 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); |
| 1459 | 1694 | ||
| 1695 | // Kept for the wizard's "Restart Now": relaunch with the same command line. | ||
| 1696 | g_restartArgc = argc; | ||
| 1697 | g_restartArgv = argv; | ||
| 1698 | |||
| 1699 | // First run on a Berkeley DB wallet with no recorded choice yet: offer to | ||
| 1700 | // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists, | ||
| 1701 | // or once a choice has been recorded in the backend marker. | ||
| 1702 | if (!WalletSQLiteRuntimeActive() && | ||
| 1703 | FileExists((GetAppDir() + "/wallet.dat").c_str()) && | ||
| 1704 | !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) && | ||
| 1705 | ReadWalletBackendMarker().empty()) | ||
| 1706 | { | ||
| 1707 | g_showBackendWizard = true; | ||
| 1708 | } | ||
| 1709 | |||
| 1460 | int frame = 0; | 1710 | int frame = 0; |
| 1461 | while (!glfwWindowShouldClose(window) && !fShutdown) | 1711 | while (!glfwWindowShouldClose(window) && !fShutdown) |
| 1462 | { | 1712 | { |
| @@ -1480,6 +1730,7 @@ int RunGUI(int argc, char* argv[]) | |||
| 1480 | DrawRestorePhraseDialog(); | 1730 | DrawRestorePhraseDialog(); |
| 1481 | DrawDiagnosticsDialog(); | 1731 | DrawDiagnosticsDialog(); |
| 1482 | DrawAboutDialog(); | 1732 | DrawAboutDialog(); |
| 1733 | DrawBackendWizardDialog(); | ||
| 1483 | DrawStatusBar(); | 1734 | DrawStatusBar(); |
| 1484 | 1735 | ||
| 1485 | ImGui::Render(); | 1736 | ImGui::Render(); |
diff --git a/src/headers_core.h b/src/headers_core.h index 6a60b52..2824126 100644 --- a/src/headers_core.h +++ b/src/headers_core.h | |||
| @@ -1,6 +1,11 @@ | |||
| 1 | // Core node headers -- no GUI framework dependency. | 1 | // Core node headers -- no GUI framework dependency. |
| 2 | // Included by gui.cpp and all non-GUI source files. | 2 | // Included by gui.cpp and all non-GUI source files. |
| 3 | 3 | ||
| 4 | // The one place the human-readable release version lives -- bump it at release. | ||
| 5 | // It is what the About dialog shows. Protocol and transaction versions elsewhere | ||
| 6 | // (nVersion fields) are unrelated and must not be changed for a release. | ||
| 7 | #define BITFLASH_VERSION_STRING "1.2.20" | ||
| 8 | |||
| 4 | #ifdef _MSC_VER | 9 | #ifdef _MSC_VER |
| 5 | #pragma warning(disable:4786) | 10 | #pragma warning(disable:4786) |
| 6 | #endif | 11 | #endif |
diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..7398064 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <atomic> | 14 | #include <atomic> |
| 15 | #include <mutex> | 15 | #include <mutex> |
| 16 | #include <thread> | 16 | #include <thread> |
| 17 | #include <system_error> | ||
| 17 | #include "bip32.h" | 18 | #include "bip32.h" |
| 18 | #include "btfaddr.h" | 19 | #include "btfaddr.h" |
| 19 | #include "btftunnel.h" | 20 | #include "btftunnel.h" |
| @@ -64,6 +65,137 @@ CBlockIndex* pindexBest = NULL; | |||
| 64 | map<uint256, CBlock*> mapOrphanBlocks; | 65 | map<uint256, CBlock*> mapOrphanBlocks; |
| 65 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; | 66 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; |
| 66 | 67 | ||
| 68 | // Block bodies we have sent a getdata for and not yet received, so the | ||
| 69 | // failover pass in SendMessages can tell "requested and being served" from | ||
| 70 | // "requested and gone quiet" and re-issue the request to a different peer | ||
| 71 | // (#33). Keyed by block hash; setAsked is the peers already tried since the | ||
| 72 | // round last restarted, so the retry rotates instead of hammering the | ||
| 73 | // announcer that is not serving. Guarded by cs_main, which both the message | ||
| 74 | // handlers and SendMessages already hold. | ||
| 75 | // | ||
| 76 | // Capped small on purpose. At the tip there is one block in flight; during | ||
| 77 | // initial download there are legitimately hundreds outstanding to one peer, | ||
| 78 | // and rotating those would duplicate the whole download to every other peer. | ||
| 79 | // | ||
| 80 | // The cap is a shared resource and nothing on the path from inv to insertion | ||
| 81 | // can tell a real block hash from 32 random bytes -- AlreadyHave only asks | ||
| 82 | // whether we already hold the block, and there is no header to check work | ||
| 83 | // against. So the table does not refuse a new hash when it is full; it makes | ||
| 84 | // room. See MakeRoomInBlockFetch for the rule, which is what stops one peer | ||
| 85 | // filling all eight slots and switching failover off for everybody. | ||
| 86 | // | ||
| 87 | // The initialisers matter: the insertion path uses operator[] and then tests | ||
| 88 | // nTimeFirstAsked == 0 to detect a fresh entry. That worked on | ||
| 89 | // value-initialisation alone, but only for as long as nobody gave this struct | ||
| 90 | // a constructor. | ||
| 91 | struct CBlockFetch | ||
| 92 | { | ||
| 93 | int64 nTimeFirstAsked = 0; // when the first getdata for this block went out | ||
| 94 | int64 nTimeAsked = 0; // when the latest getdata went out | ||
| 95 | CAddress addrAnnounced; // peer whose announcement put this hash here | ||
| 96 | set<CAddress> setAsked; // peers tried since the round last restarted | ||
| 97 | }; | ||
| 98 | map<uint256, CBlockFetch> mapBlockFetch; | ||
| 99 | static const int BLOCK_FETCH_TRACK_MAX = 8; | ||
| 100 | // A block nobody has served for this long is not a fetch problem: the hash may | ||
| 101 | // have been announced and never existed. Drop the entry rather than getdata | ||
| 102 | // forever; a re-announcement recreates it. | ||
| 103 | static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60; | ||
| 104 | |||
| 105 | // Failover firing at all is worth saying out loud: it means peers are | ||
| 106 | // announcing blocks they then do not serve, which is the pathology of #33 and | ||
| 107 | // the thing nobody could otherwise see. Naming the peer on every re-request is | ||
| 108 | // a different matter. A peer can drive that line as often as it likes by | ||
| 109 | // announcing hashes it will not serve, and what it prints is a peer address, | ||
| 110 | // which is the one thing the .btf transport otherwise takes trouble not to | ||
| 111 | // spread around. So the per-peer detail sits behind the net category and the | ||
| 112 | // unconditional line is a count over a window no peer can shorten: the first | ||
| 113 | // failover after a quiet spell prints at once, because the change from "blocks | ||
| 114 | // arrive" to "blocks are announced and not served" is the event worth seeing, | ||
| 115 | // and everything after it is summarised once a minute. | ||
| 116 | static const int BLOCK_FETCH_REPORT_SECS = 60; | ||
| 117 | static int64 nBlockFetchWindowStart = 0; | ||
| 118 | static int nBlockFetchFailovers = 0; | ||
| 119 | |||
| 120 | // How stale our own best block may be before we stop calling this the tip. | ||
| 121 | // Blocks target two minutes, so this is thirty intervals: far longer than any | ||
| 122 | // stall worth failing over from -- the one in #33 ran sixteen minutes -- and | ||
| 123 | // far shorter than catching up, which starts hours or days back. | ||
| 124 | static const int BLOCK_FETCH_TIP_AGE_SECS = 60 * 60; | ||
| 125 | |||
| 126 | // Whether this node is tracking the tip rather than still catching up. | ||
| 127 | // | ||
| 128 | // Measured off the age of our own best block, not off any height a peer told | ||
| 129 | // us. nStartingHeight is optional in the version message and every node | ||
| 130 | // released so far sends the short form, so it is -1 for essentially every | ||
| 131 | // peer; and where it is filled in later it is filled in from the height of a | ||
| 132 | // block that peer served us, which during catch-up tracks our own height and | ||
| 133 | // would report "at the tip" for the whole download. Block timestamps are the | ||
| 134 | // one clock here that peers cannot move: CheckBlock refuses anything more than | ||
| 135 | // two hours ahead and AcceptBlock refuses anything at or before the previous | ||
| 136 | // median, so the only way to make this read "at the tip" falsely is to mine, | ||
| 137 | // and all that buys is the 15s spacing this code had unconditionally anyway. | ||
| 138 | bool IsBlockFetchNearTip() | ||
| 139 | { | ||
| 140 | return pindexBest && (int64)pindexBest->nTime > GetTime() - BLOCK_FETCH_TIP_AGE_SECS; | ||
| 141 | } | ||
| 142 | |||
| 143 | // Drop one entry so a hash announced by addrNew can be tracked. | ||
| 144 | // | ||
| 145 | // Refusing the insertion instead was a remote off-switch for the whole | ||
| 146 | // feature. An inv carrying eight hashes that never existed filled every slot | ||
| 147 | // -- nothing between inv and here can tell those from real blocks -- and no | ||
| 148 | // genuinely new block was tracked again until the give-up timer expired ten | ||
| 149 | // minutes later, re-armable for one message per ten minutes from a single | ||
| 150 | // connection. | ||
| 151 | // | ||
| 152 | // So evict instead, by occupancy: a peer may only take a slot from a peer | ||
| 153 | // holding strictly more slots than itself, and otherwise recycles one of its | ||
| 154 | // own. That is what keeps eviction from becoming the same attack in a new | ||
| 155 | // shape. A peer that has filled the table is by definition the heaviest | ||
| 156 | // holder, so its own entries are the only ones it can displace, while a peer | ||
| 157 | // announcing its first block holds nothing and therefore always evicts from | ||
| 158 | // the heaviest -- a genuine announcement is never refused a slot, whatever the | ||
| 159 | // attacker holds or however many connections it holds it from. The one case | ||
| 160 | // that costs a real fetch is eight distinct peers holding one entry each, | ||
| 161 | // where a ninth takes the oldest; that is a busy healthy node, not an attack, | ||
| 162 | // and losing the oldest of eight beats refusing the ninth. | ||
| 163 | static void MakeRoomInBlockFetch(const CAddress& addrNew) | ||
| 164 | { | ||
| 165 | if (mapBlockFetch.empty()) | ||
| 166 | return; | ||
| 167 | |||
| 168 | map<CAddress, int> mapHeld; | ||
| 169 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi) | ||
| 170 | mapHeld[(*mi).second.addrAnnounced]++; | ||
| 171 | |||
| 172 | int nMine = mapHeld.count(addrNew) ? mapHeld[addrNew] : 0; | ||
| 173 | |||
| 174 | int nMax = 0; | ||
| 175 | CAddress addrVictim; | ||
| 176 | for (map<CAddress, int>::iterator mi = mapHeld.begin(); mi != mapHeld.end(); ++mi) | ||
| 177 | if ((*mi).second > nMax) | ||
| 178 | { | ||
| 179 | nMax = (*mi).second; | ||
| 180 | addrVictim = (*mi).first; | ||
| 181 | } | ||
| 182 | |||
| 183 | // Nobody is holding more than this peer, so it recycles its own oldest | ||
| 184 | // rather than taking a slot off an equal. | ||
| 185 | if (nMax <= nMine) | ||
| 186 | addrVictim = addrNew; | ||
| 187 | |||
| 188 | map<uint256, CBlockFetch>::iterator miVictim = mapBlockFetch.end(); | ||
| 189 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi) | ||
| 190 | if ((*mi).second.addrAnnounced == addrVictim && | ||
| 191 | (miVictim == mapBlockFetch.end() || | ||
| 192 | (*mi).second.nTimeFirstAsked < (*miVictim).second.nTimeFirstAsked)) | ||
| 193 | miVictim = mi; | ||
| 194 | |||
| 195 | if (miVictim != mapBlockFetch.end()) | ||
| 196 | mapBlockFetch.erase(miVictim); | ||
| 197 | } | ||
| 198 | |||
| 67 | map<uint256, CDataStream*> mapOrphanTransactions; | 199 | map<uint256, CDataStream*> mapOrphanTransactions; |
| 68 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; | 200 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; |
| 69 | 201 | ||
| @@ -1712,7 +1844,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb) | |||
| 1712 | uint256 hash = GetHash(); | 1844 | uint256 hash = GetHash(); |
| 1713 | if (!txdb.ContainsTx(hash)) | 1845 | if (!txdb.ContainsTx(hash)) |
| 1714 | { | 1846 | { |
| 1715 | if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); | 1847 | if (LogAcceptsCategory("relay")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); |
| 1716 | RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); | 1848 | RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); |
| 1717 | } | 1849 | } |
| 1718 | } | 1850 | } |
| @@ -2678,7 +2810,18 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2678 | // CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) | 2810 | // CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) |
| 2679 | // vMerkleTree: 4a5e1e | 2811 | // vMerkleTree: 4a5e1e |
| 2680 | 2812 | ||
| 2681 | // Bitflash genesis block -- own header (fair launch, 2026 stable relaunch) | 2813 | // Bitflash genesis block -- own header (fair launch) |
| 2814 | // | ||
| 2815 | // Do not change pszTimestamp, the scriptPubKey, nTime or nNonce. They | ||
| 2816 | // are the inputs to the genesis hash, and the genesis hash is | ||
| 2817 | // consensus. Changing one on its own trips the assert below, which is | ||
| 2818 | // loud and reverts cleanly; changing one and regenerating | ||
| 2819 | // GENESIS_NONCE and hashGenesisBlock to match -- which is exactly what | ||
| 2820 | // the re-mining branch below prints out and invites -- gives a node | ||
| 2821 | // that starts without complaint, builds its own chain from height 0, | ||
| 2822 | // and rejects every real peer as being on a different chain. That one | ||
| 2823 | // fails silently, and the damage scales with how many people run the | ||
| 2824 | // "fixed" build. | ||
| 2682 | char* pszTimestamp = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"; | 2825 | char* pszTimestamp = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"; |
| 2683 | CTransaction txNew; | 2826 | CTransaction txNew; |
| 2684 | txNew.vin.resize(1); | 2827 | txNew.vin.resize(1); |
| @@ -2691,7 +2834,14 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2691 | block.hashPrevBlock = 0; | 2834 | block.hashPrevBlock = 0; |
| 2692 | block.hashMerkleRoot = block.BuildMerkleTree(); | 2835 | block.hashMerkleRoot = block.BuildMerkleTree(); |
| 2693 | block.nVersion = 1; | 2836 | block.nVersion = 1; |
| 2694 | block.nTime = 1753315200; // 2026-07-24 00:00:00 UTC (stable relaunch) | 2837 | // 1753315200 is 2025-07-24T00:00:00Z. The headline in pszTimestamp |
| 2838 | // above reads 2026 and does not match it. This is a known discrepancy | ||
| 2839 | // and not a typo in the constant: the value is only consensus-critical | ||
| 2840 | // as an input to the genesis hash, the block hashes to | ||
| 2841 | // hashGenesisBlock with it, and every node on the network agrees. The | ||
| 2842 | // mismatch is in the launched chain and cannot be corrected -- see the | ||
| 2843 | // warning above before deciding otherwise. | ||
| 2844 | block.nTime = 1753315200; | ||
| 2695 | block.nBits = 0x1f0fffff; // = bnProofOfWorkLimit (Bitflash RandomX floor) | 2845 | block.nBits = 0x1f0fffff; // = bnProofOfWorkLimit (Bitflash RandomX floor) |
| 2696 | block.nNonce = GENESIS_NONCE; | 2846 | block.nNonce = GENESIS_NONCE; |
| 2697 | 2847 | ||
| @@ -2718,6 +2868,11 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2718 | printf("powhash = %s\n", block.GetPoWHash().ToString().c_str()); | 2868 | printf("powhash = %s\n", block.GetPoWHash().ToString().c_str()); |
| 2719 | printf("hash = %s\n", block.GetHash().ToString().c_str()); | 2869 | printf("hash = %s\n", block.GetHash().ToString().c_str()); |
| 2720 | printf("merkle = %s\n", block.hashMerkleRoot.ToString().c_str()); | 2870 | printf("merkle = %s\n", block.hashMerkleRoot.ToString().c_str()); |
| 2871 | printf("----------------------------------------\n"); | ||
| 2872 | printf("These constants bring up a NEW chain. Do not paste them\n"); | ||
| 2873 | printf("into a build for this network to make an edited nTime or\n"); | ||
| 2874 | printf("pszTimestamp fit -- a node whose genesis differs starts\n"); | ||
| 2875 | printf("cleanly on a chain of its own and rejects every real peer.\n"); | ||
| 2721 | printf("========================================\n"); | 2876 | printf("========================================\n"); |
| 2722 | } | 2877 | } |
| 2723 | 2878 | ||
| @@ -3263,15 +3418,61 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3263 | vRecv >> *pblock; | 3418 | vRecv >> *pblock; |
| 3264 | 3419 | ||
| 3265 | //// debug print | 3420 | //// debug print |
| 3266 | if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); } | 3421 | if (LogAcceptsCategory("relay")) { printf("received block:\n"); pblock->print(); } |
| 3267 | 3422 | ||
| 3268 | CInv inv(MSG_BLOCK, pblock->GetHash()); | 3423 | CInv inv(MSG_BLOCK, pblock->GetHash()); |
| 3269 | pfrom->AddInventoryKnown(inv); | 3424 | pfrom->AddInventoryKnown(inv); |
| 3270 | 3425 | ||
| 3426 | // GetHash() covers the 80-byte header and nothing else, so the hash | ||
| 3427 | // coming out right does not make this the block we asked for. Every | ||
| 3428 | // peer that saw X announced holds X's header, and can answer our | ||
| 3429 | // getdata with that header bolted onto a transaction list of its own | ||
| 3430 | // invention -- the reply still hashes to X. hashMerkleRoot is the only | ||
| 3431 | // thing binding the header to the body, so check it here, before the | ||
| 3432 | // mapBlockFetch entry is touched and before CheckBlock spends a | ||
| 3433 | // RandomX hash on a body already known to be forged. | ||
| 3434 | // | ||
| 3435 | // There is no honest way to arrive here. Peer traffic rides the .btf | ||
| 3436 | // channel, which is XChaCha20-Poly1305, so a body that arrives at all | ||
| 3437 | // arrived intact: a mismatch is the peer's own construction, not line | ||
| 3438 | // noise. Drop the peer and leave the fetch outstanding so the failover | ||
| 3439 | // sweep asks somebody else for the real X, rather than recording X as | ||
| 3440 | // delivered on the strength of a header anyone could copy. | ||
| 3441 | if (pblock->hashMerkleRoot != pblock->BuildMerkleTree()) | ||
| 3442 | { | ||
| 3443 | if (LogAcceptsCategory("net")) | ||
| 3444 | printf("peer %s served a forged body for %s\n", | ||
| 3445 | pfrom->addr.ToString().c_str(), inv.ToString().c_str()); | ||
| 3446 | // One line per connection: ProcessMessages keeps draining the | ||
| 3447 | // receive buffer after fDisconnect is set, so a peer that packs | ||
| 3448 | // many forged bodies into one buffer must not get one log line | ||
| 3449 | // for each of them. | ||
| 3450 | if (!pfrom->fDisconnect) | ||
| 3451 | printf("block fetch: a peer answered the request for %s with a body that is not " | ||
| 3452 | "that block -- dropping it\n", inv.hash.ToString().substr(0,14).c_str()); | ||
| 3453 | pfrom->fDisconnect = true; | ||
| 3454 | return error("ProcessMessage() : forged block body for %s", | ||
| 3455 | inv.hash.ToString().substr(0,14).c_str()); | ||
| 3456 | } | ||
| 3457 | |||
| 3271 | if (ProcessBlock(pfrom, pblock.release())) | 3458 | if (ProcessBlock(pfrom, pblock.release())) |
| 3272 | { | 3459 | { |
| 3273 | mapAlreadyAskedFor.erase(inv); | 3460 | mapAlreadyAskedFor.erase(inv); |
| 3274 | 3461 | ||
| 3462 | // The body arrived and validated, so the fetch really is over: | ||
| 3463 | // the block is now in mapBlockIndex, or parked in mapOrphanBlocks, | ||
| 3464 | // and either way AlreadyHave answers for it from here on. | ||
| 3465 | // | ||
| 3466 | // This has to stay inside the success branch, next to the erase | ||
| 3467 | // above. When validation rejects a body nothing is stored, | ||
| 3468 | // AlreadyHave stays false, and the mapAskFor entry was already | ||
| 3469 | // consumed when the getdata went out -- so erasing the tracking | ||
| 3470 | // here would leave the block requested from nobody at all until | ||
| 3471 | // some peer happened to re-announce it. That is exactly the stall | ||
| 3472 | // of #33, and erasing before ProcessBlock made it reachable on | ||
| 3473 | // demand. | ||
| 3474 | mapBlockFetch.erase(inv.hash); | ||
| 3475 | |||
| 3275 | // A peer that just handed us a block is demonstrably at least at | 3476 | // A peer that just handed us a block is demonstrably at least at |
| 3276 | // that block's height. Without this, nStartingHeight is a snapshot | 3477 | // that block's height. Without this, nStartingHeight is a snapshot |
| 3277 | // taken once during the version handshake and never touched again, | 3478 | // taken once during the version handshake and never touched again, |
| @@ -3396,12 +3597,119 @@ bool SendMessages(CNode* pto) | |||
| 3396 | const CInv& inv = (*pto->mapAskFor.begin()).second; | 3597 | const CInv& inv = (*pto->mapAskFor.begin()).second; |
| 3397 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); | 3598 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); |
| 3398 | if (!AlreadyHave(txdb, inv)) | 3599 | if (!AlreadyHave(txdb, inv)) |
| 3600 | { | ||
| 3399 | vAskFor.push_back(inv); | 3601 | vAskFor.push_back(inv); |
| 3602 | // Blocks get their delivery tracked so the failover pass | ||
| 3603 | // below can chase the ones that go quiet. | ||
| 3604 | if (inv.type == MSG_BLOCK) | ||
| 3605 | { | ||
| 3606 | // Full, and this is a hash we are not already tracking: | ||
| 3607 | // make room rather than drop it on the floor, or one peer | ||
| 3608 | // announcing eight hashes turns failover off for everyone. | ||
| 3609 | if (!mapBlockFetch.count(inv.hash) && | ||
| 3610 | mapBlockFetch.size() >= BLOCK_FETCH_TRACK_MAX) | ||
| 3611 | MakeRoomInBlockFetch(pto->addr); | ||
| 3612 | |||
| 3613 | if (mapBlockFetch.count(inv.hash) || | ||
| 3614 | mapBlockFetch.size() < BLOCK_FETCH_TRACK_MAX) | ||
| 3615 | { | ||
| 3616 | CBlockFetch& fetch = mapBlockFetch[inv.hash]; | ||
| 3617 | if (fetch.nTimeFirstAsked == 0) | ||
| 3618 | { | ||
| 3619 | fetch.nTimeFirstAsked = GetTime(); | ||
| 3620 | // Who is answerable for this hash existing. Set | ||
| 3621 | // once, on creation: the failover sweep asks other | ||
| 3622 | // peers later, and none of them chose to announce | ||
| 3623 | // it, so none of them should be charged for it. | ||
| 3624 | fetch.addrAnnounced = pto->addr; | ||
| 3625 | } | ||
| 3626 | fetch.nTimeAsked = GetTime(); | ||
| 3627 | fetch.setAsked.insert(pto->addr); | ||
| 3628 | } | ||
| 3629 | } | ||
| 3630 | } | ||
| 3400 | pto->mapAskFor.erase(pto->mapAskFor.begin()); | 3631 | pto->mapAskFor.erase(pto->mapAskFor.begin()); |
| 3401 | } | 3632 | } |
| 3402 | if (!vAskFor.empty()) | 3633 | if (!vAskFor.empty()) |
| 3403 | pto->PushMessage("getdata", vAskFor); | 3634 | pto->PushMessage("getdata", vAskFor); |
| 3404 | 3635 | ||
| 3636 | |||
| 3637 | // | ||
| 3638 | // Message: getdata (block fetch failover) | ||
| 3639 | // | ||
| 3640 | // A requested block that has not arrived in BLOCK_FETCH_RETRY_SECS is | ||
| 3641 | // not still in transit -- a body is at most 1 MB and somebody | ||
| 3642 | // demonstrably has it -- so ask somebody else instead of waiting on | ||
| 3643 | // whoever was asked first (#33). Each peer the message handler visits | ||
| 3644 | // takes at most one turn per block per round; once every connected | ||
| 3645 | // peer has had its turn and a further interval passes with nobody | ||
| 3646 | // new to ask, the round restarts. A peer that declared itself a | ||
| 3647 | // client cannot serve block bodies and is no use as a target. | ||
| 3648 | if (!pto->fClient) | ||
| 3649 | { | ||
| 3650 | vector<CInv> vFailover; | ||
| 3651 | int64 nNowSecs = GetTime(); | ||
| 3652 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end();) | ||
| 3653 | { | ||
| 3654 | CBlockFetch& fetch = (*mi).second; | ||
| 3655 | CInv inv(MSG_BLOCK, (*mi).first); | ||
| 3656 | if (AlreadyHave(txdb, inv)) | ||
| 3657 | { | ||
| 3658 | // Arrived by some other road -- our own miner, an orphan | ||
| 3659 | // parked in mapOrphanBlocks. Nothing left to chase. | ||
| 3660 | mapBlockFetch.erase(mi++); | ||
| 3661 | continue; | ||
| 3662 | } | ||
| 3663 | if (nNowSecs - fetch.nTimeFirstAsked > BLOCK_FETCH_GIVEUP_SECS) | ||
| 3664 | { | ||
| 3665 | printf("block fetch: giving up on %s -- no peer served it in %d s\n", | ||
| 3666 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeFirstAsked)); | ||
| 3667 | mapBlockFetch.erase(mi++); | ||
| 3668 | continue; | ||
| 3669 | } | ||
| 3670 | if (nNowSecs - fetch.nTimeAsked >= BLOCK_FETCH_RETRY_SECS) | ||
| 3671 | { | ||
| 3672 | // The handler loop visits every peer within ~100ms, so a | ||
| 3673 | // request stale by two full intervals means nobody outside | ||
| 3674 | // setAsked was there to ask. Start the round over. | ||
| 3675 | if (nNowSecs - fetch.nTimeAsked >= 2 * BLOCK_FETCH_RETRY_SECS) | ||
| 3676 | fetch.setAsked.clear(); | ||
| 3677 | if (!fetch.setAsked.count(pto->addr)) | ||
| 3678 | { | ||
| 3679 | // Which block, how long, and which peer we are trying | ||
| 3680 | // next. Behind the category because it names a peer | ||
| 3681 | // and a peer decides how often it is written. | ||
| 3682 | if (LogAcceptsCategory("net")) | ||
| 3683 | printf("block fetch failover: %s unanswered for %d s, re-requesting from %s\n", | ||
| 3684 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeAsked), | ||
| 3685 | pto->addr.ToString().c_str()); | ||
| 3686 | |||
| 3687 | // The unconditional half: no addresses, and at most | ||
| 3688 | // one line per BLOCK_FETCH_REPORT_SECS however hard a | ||
| 3689 | // peer tries. See the counters for the reasoning. | ||
| 3690 | nBlockFetchFailovers++; | ||
| 3691 | if (nNowSecs - nBlockFetchWindowStart >= BLOCK_FETCH_REPORT_SECS) | ||
| 3692 | { | ||
| 3693 | printf("block fetch: %d block request%s went unanswered and %s been " | ||
| 3694 | "re-asked of other peers (%d tracked)\n", | ||
| 3695 | nBlockFetchFailovers, nBlockFetchFailovers == 1 ? "" : "s", | ||
| 3696 | nBlockFetchFailovers == 1 ? "has" : "have", | ||
| 3697 | (int)mapBlockFetch.size()); | ||
| 3698 | nBlockFetchWindowStart = nNowSecs; | ||
| 3699 | nBlockFetchFailovers = 0; | ||
| 3700 | } | ||
| 3701 | |||
| 3702 | vFailover.push_back(inv); | ||
| 3703 | fetch.nTimeAsked = nNowSecs; | ||
| 3704 | fetch.setAsked.insert(pto->addr); | ||
| 3705 | } | ||
| 3706 | } | ||
| 3707 | ++mi; | ||
| 3708 | } | ||
| 3709 | if (!vFailover.empty()) | ||
| 3710 | pto->PushMessage("getdata", vFailover); | ||
| 3711 | } | ||
| 3712 | |||
| 3405 | } | 3713 | } |
| 3406 | return true; | 3714 | return true; |
| 3407 | } | 3715 | } |
| @@ -3662,6 +3970,14 @@ static void ParticipantHashWorker(ParticipantJobShared* sh, int idx, int nWorker | |||
| 3662 | RandomXDestroyMinerVM(vm); | 3970 | RandomXDestroyMinerVM(vm); |
| 3663 | } | 3971 | } |
| 3664 | 3972 | ||
| 3973 | // Run one participant session: resolve the pool, open the tunnel, subscribe, | ||
| 3974 | // hash, submit shares. Returns true only if the pool actually handed out work | ||
| 3975 | // -- at least one well-formed mining.notify arrived. Everything else is false, | ||
| 3976 | // including a tunnel that opened and was dropped before any job. | ||
| 3977 | // | ||
| 3978 | // The caller's reconnect backoff keys off this return value, so any path added | ||
| 3979 | // here that leaves the session without work MUST return false. "The session | ||
| 3980 | // ended tidily" is not the question being asked. | ||
| 3665 | static bool PoolParticipantMiner() | 3981 | static bool PoolParticipantMiner() |
| 3666 | { | 3982 | { |
| 3667 | // ------------------------------------------------------------------ | 3983 | // ------------------------------------------------------------------ |
| @@ -3739,6 +4055,33 @@ static bool PoolParticipantMiner() | |||
| 3739 | } | 4055 | } |
| 3740 | LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); | 4056 | LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); |
| 3741 | 4057 | ||
| 4058 | // The descriptor names ONE meeting node, but the pool parks a | ||
| 4059 | // registration at every relay it knows (one accept thread per relay, | ||
| 4060 | // rpc.cpp:AcceptOneFn), so any seed relay can pair us. Insisting on the | ||
| 4061 | // advertised entry forever turns one bad path into a miner that idles | ||
| 4062 | // indefinitely while a working path exists (issue #17); after a few | ||
| 4063 | // consecutive tunnel failures, rotate the seed relays into the attempts | ||
| 4064 | // too. Only ONE participant session runs (StartMinerThreads), so these | ||
| 4065 | // statics are single-threaded. | ||
| 4066 | static const int TUNNEL_FAILOVER_AFTER = 3; | ||
| 4067 | static int nTunnelFails = 0; // consecutive tunnel failures, this pool | ||
| 4068 | static size_t iFailoverRelay = 0; // rotation cursor over vBtfMeetingRelays | ||
| 4069 | static std::string strFailPool; // pool the counters belong to | ||
| 4070 | if (strFailPool != strParticipantPool) { | ||
| 4071 | strFailPool = strParticipantPool; | ||
| 4072 | nTunnelFails = 0; | ||
| 4073 | iFailoverRelay = 0; | ||
| 4074 | } | ||
| 4075 | if (nTunnelFails >= TUNNEL_FAILOVER_AFTER && !vBtfMeetingRelays.empty()) { | ||
| 4076 | std::string alt = vBtfMeetingRelays[iFailoverRelay++ % vBtfMeetingRelays.size()]; | ||
| 4077 | if (alt != meetingHostPort) { | ||
| 4078 | LogPrint("worker", "[worker] advertised meeting node %s failed %d" | ||
| 4079 | " times in a row -- trying seed relay %s instead\n", | ||
| 4080 | meetingHostPort.c_str(), nTunnelFails, alt.c_str()); | ||
| 4081 | meetingHostPort = alt; | ||
| 4082 | } | ||
| 4083 | } | ||
| 4084 | |||
| 3742 | size_t colon = meetingHostPort.rfind(':'); | 4085 | size_t colon = meetingHostPort.rfind(':'); |
| 3743 | if (colon == std::string::npos) { | 4086 | if (colon == std::string::npos) { |
| 3744 | LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); | 4087 | LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); |
| @@ -3758,15 +4101,18 @@ static bool PoolParticipantMiner() | |||
| 3758 | // Step 2b: open encrypted tunnel to pool | 4101 | // Step 2b: open encrypted tunnel to pool |
| 3759 | // ------------------------------------------------------------------ | 4102 | // ------------------------------------------------------------------ |
| 3760 | SetParticipantMiningStatus("opening tunnel"); | 4103 | SetParticipantMiningStatus("opening tunnel"); |
| 4104 | btf::TunnelFail tunnelFail = btf::TUNNEL_FAIL_NONE; | ||
| 3761 | s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, | 4105 | s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, |
| 3762 | target_pubkey, service_enc_pub); | 4106 | target_pubkey, service_enc_pub, &tunnelFail); |
| 3763 | if (s == INVALID_SOCKET) { | 4107 | if (s == INVALID_SOCKET) { |
| 3764 | LogPrint("worker", "[worker] tunnel connect FAILED via %s" | 4108 | nTunnelFails++; |
| 3765 | " (relay unreachable or pool not listening)\n", | 4109 | LogPrint("worker", "[worker] tunnel connect FAILED via %s: %s\n", |
| 3766 | meetingHostPort.c_str()); | 4110 | meetingHostPort.c_str(), btf::TunnelFailText(tunnelFail)); |
| 3767 | SetParticipantMiningStatus("tunnel connect failed"); | 4111 | SetParticipantMiningStatus(std::string("tunnel connect failed: ") |
| 4112 | + btf::TunnelFailText(tunnelFail)); | ||
| 3768 | return false; | 4113 | return false; |
| 3769 | } | 4114 | } |
| 4115 | nTunnelFails = 0; | ||
| 3770 | LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); | 4116 | LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); |
| 3771 | SetParticipantMiningStatus("tunnel open"); | 4117 | SetParticipantMiningStatus("tunnel open"); |
| 3772 | } | 4118 | } |
| @@ -3899,9 +4245,32 @@ connected: | |||
| 3899 | std::vector<ParticipantShareHit> shareHits; | 4245 | std::vector<ParticipantShareHit> shareHits; |
| 3900 | std::mutex shareHitsMtx; | 4246 | std::mutex shareHitsMtx; |
| 3901 | std::vector<std::thread> hashThreads; | 4247 | std::vector<std::thread> hashThreads; |
| 3902 | for (int i = 0; i < nWorkers; i++) | 4248 | try |
| 3903 | hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, | 4249 | { |
| 3904 | &shareHits, &shareHitsMtx); | 4250 | for (int i = 0; i < nWorkers; i++) |
| 4251 | hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, | ||
| 4252 | &shareHits, &shareHitsMtx); | ||
| 4253 | } | ||
| 4254 | catch (const std::system_error& e) | ||
| 4255 | { | ||
| 4256 | // A refused pthread_create used to reach std::terminate here and | ||
| 4257 | // kill the worker mid-session -- observed twice on an otherwise | ||
| 4258 | // healthy participant. Stop the workers that did start the same way | ||
| 4259 | // the disconnect path below does, drop this pool connection, and let | ||
| 4260 | // the caller's reconnect loop try again once the pressure clears. | ||
| 4261 | LogPrint("worker", "[worker] cannot spawn hash threads (%s)" | ||
| 4262 | " -- dropping pool connection\n", e.what()); | ||
| 4263 | SetParticipantMiningStatus("hash thread spawn failed"); | ||
| 4264 | { | ||
| 4265 | std::lock_guard<std::mutex> lk(jobShared.mtx); | ||
| 4266 | jobShared.stop = true; | ||
| 4267 | jobShared.haveJob = false; | ||
| 4268 | } | ||
| 4269 | for (std::thread& t : hashThreads) | ||
| 4270 | t.join(); | ||
| 4271 | closesocket(s); | ||
| 4272 | return false; | ||
| 4273 | } | ||
| 3905 | LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", | 4274 | LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", |
| 3906 | nWorkers); | 4275 | nWorkers); |
| 3907 | const uint64 sessionStartHashes = gParticipantHashes.load(); | 4276 | const uint64 sessionStartHashes = gParticipantHashes.load(); |
| @@ -3912,8 +4281,17 @@ connected: | |||
| 3912 | std::string jobId; | 4281 | std::string jobId; |
| 3913 | unsigned char header[80] = {}; | 4282 | unsigned char header[80] = {}; |
| 3914 | uint256 target; | 4283 | uint256 target; |
| 4284 | // Cached answer to "which block is this job built on", for the | ||
| 4285 | // mining.notify handler below -- see the comment there. Session-local, so | ||
| 4286 | // a reconnect re-resolves rather than trusting an answer obtained before | ||
| 4287 | // a possible change of pool. | ||
| 4288 | uint256 hashPrevResolved; // prevhash the cached answer describes | ||
| 4289 | bool fPrevResolved = false; // whether that answer exists yet | ||
| 4290 | int nPrevHeight = -1; // its height, or -1 == unknown to us | ||
| 4291 | int nBestHeightResolved = -1; // nBestHeight when we last resolved | ||
| 3915 | double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool | 4292 | double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool |
| 3916 | bool haveJob = false; | 4293 | bool haveJob = false; |
| 4294 | bool gotAnyJob = false; // any job at all this session -- the return value | ||
| 3917 | bool gotSubscribeAck = false; | 4295 | bool gotSubscribeAck = false; |
| 3918 | bool gotAuthorizeAck = false; | 4296 | bool gotAuthorizeAck = false; |
| 3919 | int64 authWaitStart = 0; | 4297 | int64 authWaitStart = 0; |
| @@ -4030,8 +4408,9 @@ connected: | |||
| 4030 | jobId = newJobId; | 4408 | jobId = newJobId; |
| 4031 | memcpy(header, hdrBytes.data(), 80); | 4409 | memcpy(header, hdrBytes.data(), 80); |
| 4032 | memcpy(&target, tgtBytes.data(), 32); | 4410 | memcpy(&target, tgtBytes.data(), 32); |
| 4033 | haveJob = true; | 4411 | haveJob = true; |
| 4034 | nNonce = 1; | 4412 | gotAnyJob = true; |
| 4413 | nNonce = 1; | ||
| 4035 | { | 4414 | { |
| 4036 | std::lock_guard<std::mutex> lk(jobShared.mtx); | 4415 | std::lock_guard<std::mutex> lk(jobShared.mtx); |
| 4037 | memcpy(jobShared.header, header, 80); | 4416 | memcpy(jobShared.header, header, 80); |
| @@ -4040,8 +4419,64 @@ connected: | |||
| 4040 | jobShared.jobSeq++; | 4419 | jobShared.jobSeq++; |
| 4041 | jobShared.haveJob = true; | 4420 | jobShared.haveJob = true; |
| 4042 | } | 4421 | } |
| 4043 | LogPrint("worker", "[pool->worker] new job %s received\n", | 4422 | // Log the chain position the pool is |
| 4044 | jobId.c_str()); | 4423 | // building on, so a stale or private |
| 4424 | // fork is auditable from worker logs. | ||
| 4425 | // The prevhash comes from the header we | ||
| 4426 | // actually hash (== p[2] of the notify); | ||
| 4427 | // a brief cs_main lookup is safe here -- | ||
| 4428 | // this thread holds no other lock. | ||
| 4429 | // | ||
| 4430 | // Resolving it is the expensive half, and | ||
| 4431 | // this is the socket-reading thread: block | ||
| 4432 | // validation can hold cs_main for a long | ||
| 4433 | // time, and every moment spent waiting on | ||
| 4434 | // it here is a moment set_difficulty and | ||
| 4435 | // share responses are not being handled. | ||
| 4436 | // Most notifies in a round are a new job | ||
| 4437 | // on the SAME tip, so the answer is cached | ||
| 4438 | // and re-resolved only when it could come | ||
| 4439 | // out differently: a prevhash we have not | ||
| 4440 | // resolved, or one we could NOT resolve | ||
| 4441 | // last time while the chain has since | ||
| 4442 | // moved (unknown -> known). A block's | ||
| 4443 | // height is fixed once mapBlockIndex knows | ||
| 4444 | // it, so a cached "known" never goes | ||
| 4445 | // stale. nBestHeight is read unlocked on | ||
| 4446 | // purpose -- it only decides whether to | ||
| 4447 | // repeat a lookup, never what gets | ||
| 4448 | // printed, so a stale read costs at most | ||
| 4449 | // one deferred or one extra lookup. | ||
| 4450 | // The line still prints in full for every | ||
| 4451 | // job, cached or not. | ||
| 4452 | uint256 hashPrev; | ||
| 4453 | memcpy(&hashPrev, header + 4, 32); | ||
| 4454 | if (LogAcceptsCategory("worker")) | ||
| 4455 | { | ||
| 4456 | int nBestHeightNow = nBestHeight; | ||
| 4457 | if (!fPrevResolved | ||
| 4458 | || hashPrev != hashPrevResolved | ||
| 4459 | || (nPrevHeight < 0 && nBestHeightNow != nBestHeightResolved)) | ||
| 4460 | { | ||
| 4461 | nPrevHeight = -1; | ||
| 4462 | CRITICAL_BLOCK(cs_main) | ||
| 4463 | { | ||
| 4464 | map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(hashPrev); | ||
| 4465 | if (mi != mapBlockIndex.end()) | ||
| 4466 | nPrevHeight = (*mi).second->nHeight; | ||
| 4467 | } | ||
| 4468 | hashPrevResolved = hashPrev; | ||
| 4469 | nBestHeightResolved = nBestHeightNow; | ||
| 4470 | fPrevResolved = true; | ||
| 4471 | } | ||
| 4472 | if (nPrevHeight >= 0) | ||
| 4473 | LogPrint("worker", "[pool->worker] new job %s received, prevhash %s (known, job height %d)\n", | ||
| 4474 | jobId.c_str(), hashPrev.GetHex().c_str(), | ||
| 4475 | nPrevHeight + 1); | ||
| 4476 | else | ||
| 4477 | LogPrint("worker", "[pool->worker] new job %s received, prevhash %s (unknown to our chain)\n", | ||
| 4478 | jobId.c_str(), hashPrev.GetHex().c_str()); | ||
| 4479 | } | ||
| 4045 | SetParticipantMiningStatus("hashing job " + jobId); | 4480 | SetParticipantMiningStatus("hashing job " + jobId); |
| 4046 | } | 4481 | } |
| 4047 | } | 4482 | } |
| @@ -4167,7 +4602,11 @@ connected: | |||
| 4167 | (unsigned long long)hashesSinceSample, | 4602 | (unsigned long long)hashesSinceSample, |
| 4168 | (unsigned long long)sessionSharesSent, | 4603 | (unsigned long long)sessionSharesSent, |
| 4169 | (unsigned long long)sessionSharesAccepted); | 4604 | (unsigned long long)sessionSharesAccepted); |
| 4170 | return true; | 4605 | // Not "we got here", which is true of a tunnel the pool accepted and then |
| 4606 | // dropped without ever sending a job -- exactly the case the caller's | ||
| 4607 | // backoff exists for. Reaching the end of the session is not evidence the | ||
| 4608 | // session was worth anything; a job is. | ||
| 4609 | return gotAnyJob; | ||
| 4171 | } | 4610 | } |
| 4172 | 4611 | ||
| 4173 | // --------------------------------------------------------------------------- | 4612 | // --------------------------------------------------------------------------- |
| @@ -4364,11 +4803,51 @@ bool BitcoinMiner(int nThreadId) | |||
| 4364 | // Participant mode: connect to external pool instead of mining solo/operator | 4803 | // Participant mode: connect to external pool instead of mining solo/operator |
| 4365 | if (nMineMode == MINE_PARTICIPANT) | 4804 | if (nMineMode == MINE_PARTICIPANT) |
| 4366 | { | 4805 | { |
| 4806 | // An attempt that never got work backs off (10s doubling to 120s) | ||
| 4807 | // instead of hammering the same path every flat 10s, and a streak | ||
| 4808 | // lasting minutes is said at printf level: the identical once-per-10s | ||
| 4809 | // LogPrint line is how a worker idled all night unnoticed (issue #17). | ||
| 4810 | // | ||
| 4811 | // Only a session the pool actually sent a job to resets either. The | ||
| 4812 | // weaker test -- "the session returned" -- was gameable: a pool that | ||
| 4813 | // completes the tunnel and drops without ever sending work leaves by | ||
| 4814 | // the same tidy cleanup path a healthy one does, which would pin the | ||
| 4815 | // retry at 10s forever and defeat the backoff in exactly the scenario | ||
| 4816 | // it exists for. | ||
| 4817 | int nRetrySecs = 10; | ||
| 4818 | int64 nFailingSince = 0; | ||
| 4367 | while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { | 4819 | while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { |
| 4368 | PoolParticipantMiner(); | 4820 | bool fGotWork = PoolParticipantMiner(); |
| 4369 | if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { | 4821 | if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { |
| 4370 | LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n"); | 4822 | if (fGotWork) { |
| 4371 | for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); | 4823 | nRetrySecs = 10; |
| 4824 | nFailingSince = 0; | ||
| 4825 | } else { | ||
| 4826 | if (nFailingSince == 0) | ||
| 4827 | nFailingSince = GetTime(); | ||
| 4828 | int64 nMins = (GetTime() - nFailingSince) / 60; | ||
| 4829 | if (nMins >= 1) | ||
| 4830 | printf("[worker] connection to pool %s has been failing" | ||
| 4831 | " for %lld minutes\n", | ||
| 4832 | strParticipantPool.c_str(), (long long)nMins); | ||
| 4833 | } | ||
| 4834 | LogPrint("worker", "PoolParticipantMiner: reconnecting in %ds\n", | ||
| 4835 | nRetrySecs); | ||
| 4836 | // Poll everything the enclosing while polls, not just | ||
| 4837 | // fShutdown. Raising the ceiling to 120s made a | ||
| 4838 | // shutdown-only predicate expensive: after the operator | ||
| 4839 | // clears Generate or switches to Relay, the session -- and | ||
| 4840 | // the vfThreadRunning[3] slot that gates restarting it -- | ||
| 4841 | // would stay held for up to two minutes. Measured 119s | ||
| 4842 | // before, 1s after. | ||
| 4843 | for (int i = 0; i < nRetrySecs && !fShutdown && fGenerateBitcoins | ||
| 4844 | && nMineMode == MINE_PARTICIPANT; i++) | ||
| 4845 | Sleep(1000); | ||
| 4846 | if (!fGotWork && nRetrySecs < 120) { | ||
| 4847 | nRetrySecs *= 2; | ||
| 4848 | if (nRetrySecs > 120) | ||
| 4849 | nRetrySecs = 120; | ||
| 4850 | } | ||
| 4372 | } | 4851 | } |
| 4373 | } | 4852 | } |
| 4374 | return true; | 4853 | return true; |
| @@ -4470,7 +4949,6 @@ bool BitcoinMiner(int nThreadId) | |||
| 4470 | map<uint256, CTxIndex> mapTestPool; | 4949 | map<uint256, CTxIndex> mapTestPool; |
| 4471 | vector<char> vfAlreadyAdded(mapTransactions.size()); | 4950 | vector<char> vfAlreadyAdded(mapTransactions.size()); |
| 4472 | bool fFoundSomething = true; | 4951 | bool fFoundSomething = true; |
| 4473 | unsigned int nBlockSize = 0; | ||
| 4474 | // Assembly must stop below the consensus cap that CheckBlock() | 4952 | // Assembly must stop below the consensus cap that CheckBlock() |
| 4475 | // enforces, or we mine a block our own node then rejects. | 4953 | // enforces, or we mine a block our own node then rejects. |
| 4476 | // | 4954 | // |
| @@ -4481,9 +4959,30 @@ bool BitcoinMiner(int nThreadId) | |||
| 4481 | // meant solving a block and throwing it away -- with this pool at | 4959 | // meant solving a block and throwing it away -- with this pool at |
| 4482 | // ~90% of hashrate, that is the network stalling, not just us. | 4960 | // ~90% of hashrate, that is the network stalling, not just us. |
| 4483 | // | 4961 | // |
| 4484 | // Half the cap, keeping the original margin: the test runs before | 4962 | // The budget test then sat only on this outer while, and the |
| 4485 | // a transaction is added, so the loop can overshoot by one tx. | 4963 | // comment claimed that bounded the overshoot to a single |
| 4486 | while (fFoundSomething && nBlockSize < MAX_BLOCK_SIZE/2) | 4964 | // transaction. It did not. The inner for walks all of |
| 4965 | // mapTransactions with no test of its own, so one pass could append | ||
| 4966 | // the entire mempool: the overshoot was bounded by mempool size, | ||
| 4967 | // which is to say not bounded at all. Ask per transaction, before | ||
| 4968 | // appending, and the overshoot is zero rather than small. | ||
| 4969 | // | ||
| 4970 | // Seeded with what the block already weighs -- header, tx count, | ||
| 4971 | // coinbase -- because CheckBlock() weighs the whole serialized | ||
| 4972 | // block and not just the transactions we chose to add to it. | ||
| 4973 | unsigned int nBlockSize = ::GetSerializeSize(*pblock, SER_DISK); | ||
| 4974 | // Sigops are a budget too, and CheckBlock() enforces one that no | ||
| 4975 | // assembler here counted against. Bytes do not bound validation | ||
| 4976 | // cost: every OP_CHECKSIG is an elliptic-curve verification, so a | ||
| 4977 | // template well inside the size limit can still be refused for | ||
| 4978 | // demanding more work than a block is allowed to. Counted with | ||
| 4979 | // CTransaction::GetSigOpCount, the function CheckBlock() itself | ||
| 4980 | // sums, so the two cannot disagree about what a transaction costs. | ||
| 4981 | // | ||
| 4982 | // Seeded from the coinbase because CheckBlock() sums over all of | ||
| 4983 | // vtx, coinbase included. | ||
| 4984 | unsigned int nBlockSigOps = pblock->vtx[0].GetSigOpCount(); | ||
| 4985 | while (fFoundSomething && nBlockSize < MAX_BLOCK_SIZE_GEN) | ||
| 4487 | { | 4986 | { |
| 4488 | fFoundSomething = false; | 4987 | fFoundSomething = false; |
| 4489 | unsigned int n = 0; | 4988 | unsigned int n = 0; |
| @@ -4495,6 +4994,29 @@ bool BitcoinMiner(int nThreadId) | |||
| 4495 | if (tx.IsCoinBase() || !tx.IsFinal()) | 4994 | if (tx.IsCoinBase() || !tx.IsFinal()) |
| 4496 | continue; | 4995 | continue; |
| 4497 | 4996 | ||
| 4997 | // Skip, not break: a transaction too big for the room left | ||
| 4998 | // says nothing about the next one, which may still fit. | ||
| 4999 | // | ||
| 5000 | // This is also the only ceiling on a single transaction's | ||
| 5001 | // size anywhere on the path into a block. CheckTransaction | ||
| 5002 | // does not cap it and neither does AcceptTransaction, so | ||
| 5003 | // the mempool will hold one of any size the wire allows; | ||
| 5004 | // without this test a lone 1.5 MB transaction offering a | ||
| 5005 | // fat fee is selected, and every template built from that | ||
| 5006 | // mempool is one CheckBlock() throws away. The fee is never | ||
| 5007 | // paid, because the block never confirms. | ||
| 5008 | // | ||
| 5009 | // Deliberately before ConnectInputs and not after: | ||
| 5010 | // ConnectInputs adds this transaction's fee to nFees by | ||
| 5011 | // reference on success. Passing it and then declining the | ||
| 5012 | // transaction would credit the coinbase for a fee no | ||
| 5013 | // transaction in the block pays, and the block would be | ||
| 5014 | // rejected for a wholly different reason. | ||
| 5015 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 5016 | unsigned int nTxSigOps = tx.GetSigOpCount(); | ||
| 5017 | if (!BlockTemplateFits(nBlockSize, nBlockSigOps, nTxSize, nTxSigOps)) | ||
| 5018 | continue; | ||
| 5019 | |||
| 4498 | // Transaction fee requirements, mainly only needed for flood control | 5020 | // Transaction fee requirements, mainly only needed for flood control |
| 4499 | // Under 10K (about 80 inputs) is free for first 100 transactions | 5021 | // Under 10K (about 80 inputs) is free for first 100 transactions |
| 4500 | // Base rate is 0.01 per KB | 5022 | // Base rate is 0.01 per KB |
| @@ -4506,7 +5028,8 @@ bool BitcoinMiner(int nThreadId) | |||
| 4506 | swap(mapTestPool, mapTestPoolTmp); | 5028 | swap(mapTestPool, mapTestPoolTmp); |
| 4507 | 5029 | ||
| 4508 | pblock->vtx.push_back(tx); | 5030 | pblock->vtx.push_back(tx); |
| 4509 | nBlockSize += ::GetSerializeSize(tx, SER_NETWORK); | 5031 | nBlockSize += nTxSize; |
| 5032 | nBlockSigOps += nTxSigOps; | ||
| 4510 | vfAlreadyAdded[n] = true; | 5033 | vfAlreadyAdded[n] = true; |
| 4511 | fFoundSomething = true; | 5034 | fFoundSomething = true; |
| 4512 | } | 5035 | } |
| @@ -4555,7 +5078,7 @@ bool BitcoinMiner(int nThreadId) | |||
| 4555 | //// debug print | 5078 | //// debug print |
| 4556 | if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); | 5079 | if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); |
| 4557 | if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); | 5080 | if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); |
| 4558 | if (LogAcceptsCategory("net")) pblock->print(); | 5081 | if (LogAcceptsCategory("relay")) pblock->print(); |
| 4559 | 5082 | ||
| 4560 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); | 5083 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); |
| 4561 | CRITICAL_BLOCK(cs_main) | 5084 | CRITICAL_BLOCK(cs_main) |
| @@ -4661,6 +5184,9 @@ WalletRecoveryAudit GetWalletRecoveryAudit() | |||
| 4661 | CRITICAL_BLOCK(cs_keyPool) | 5184 | CRITICAL_BLOCK(cs_keyPool) |
| 4662 | { | 5185 | { |
| 4663 | audit.fHaveSeed = HaveHDSeed(); | 5186 | audit.fHaveSeed = HaveHDSeed(); |
| 5187 | audit.fSeedEncryptedLocked = !audit.fHaveSeed && | ||
| 5188 | (!vchCryptedHDMaster.empty() || | ||
| 5189 | !vchCryptedHDChainCode.empty()); | ||
| 4664 | audit.nSchema = nHDKeySchema; | 5190 | audit.nSchema = nHDKeySchema; |
| 4665 | audit.nDerivedKnown = nHDNext; | 5191 | audit.nDerivedKnown = nHDNext; |
| 4666 | audit.nReceiveNext = nHDReceiveNext; | 5192 | audit.nReceiveNext = nHDReceiveNext; |
| @@ -33,6 +33,35 @@ static const unsigned int MAX_BLOCK_SIGOPS = 20000; | |||
| 33 | // limit (32 MB), not a block rule. Bitcoin 0.1.0 predated the later 1 MB block | 33 | // limit (32 MB), not a block rule. Bitcoin 0.1.0 predated the later 1 MB block |
| 34 | // cap, so this tree inherited no real block-size consensus limit. | 34 | // cap, so this tree inherited no real block-size consensus limit. |
| 35 | static const unsigned int MAX_BLOCK_SIZE = 1000000; | 35 | static const unsigned int MAX_BLOCK_SIZE = 1000000; |
| 36 | // How much of that cap a template we build may fill. Half, which is what both | ||
| 37 | // assemblers already aimed at -- this only gives the number a name and one | ||
| 38 | // place to live. They had already drifted on how it was enforced, which is the | ||
| 39 | // bug this exists to stop repeating. | ||
| 40 | // | ||
| 41 | // NOT a consensus rule. CheckBlock() decides what the network accepts; this | ||
| 42 | // decides what we are willing to produce, and it has to stay under that. | ||
| 43 | static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE / 2; | ||
| 44 | |||
| 45 | // Room for one more transaction in a template that currently weighs | ||
| 46 | // nBlockSize bytes and spends nBlockSigOps signature operations? | ||
| 47 | // | ||
| 48 | // Asked BEFORE the transaction is appended, never after. A template that has | ||
| 49 | // already overshot is one CheckBlock() rejects, and asking afterwards only | ||
| 50 | // measures the waste. Both assemblers call this, so neither can drift into a | ||
| 51 | // different idea of what fits. | ||
| 52 | // | ||
| 53 | // Size gets a margin (half the cap); sigops do not. There is no cheap way to | ||
| 54 | // spend the sigop budget usefully and no reason to reserve headroom in it, so | ||
| 55 | // assembly holds itself to the same number CheckBlock() enforces -- and to the | ||
| 56 | // same comparison. CheckBlock() fails on `> MAX_BLOCK_SIGOPS`, so a block | ||
| 57 | // sitting exactly on the limit is valid and this has to accept it too. | ||
| 58 | // Assembly and validation disagreeing by one is how the size bug started. | ||
| 59 | inline bool BlockTemplateFits(unsigned int nBlockSize, unsigned int nBlockSigOps, | ||
| 60 | unsigned int nTxSize, unsigned int nTxSigOps) | ||
| 61 | { | ||
| 62 | return (nBlockSize + nTxSize <= MAX_BLOCK_SIZE_GEN && | ||
| 63 | nBlockSigOps + nTxSigOps <= MAX_BLOCK_SIGOPS); | ||
| 64 | } | ||
| 36 | static const int64 COIN = 100000000; | 65 | static const int64 COIN = 100000000; |
| 37 | // Total Bitflash emission: identical to Bitcoin (21 million). | 66 | // Total Bitflash emission: identical to Bitcoin (21 million). |
| 38 | // MAX_MONEY guards against the value overflow bug (CVE-2010-5139), which in | 67 | // MAX_MONEY guards against the value overflow bug (CVE-2010-5139), which in |
| @@ -306,6 +335,7 @@ int64 GetBalance(); | |||
| 306 | struct WalletRecoveryAudit | 335 | struct WalletRecoveryAudit |
| 307 | { | 336 | { |
| 308 | bool fHaveSeed; | 337 | bool fHaveSeed; |
| 338 | bool fSeedEncryptedLocked; | ||
| 309 | bool fDeriveComplete; | 339 | bool fDeriveComplete; |
| 310 | int nSchema; | 340 | int nSchema; |
| 311 | unsigned int nDerivedKnown; | 341 | unsigned int nDerivedKnown; |
| @@ -325,6 +355,7 @@ struct WalletRecoveryAudit | |||
| 325 | WalletRecoveryAudit() | 355 | WalletRecoveryAudit() |
| 326 | { | 356 | { |
| 327 | fHaveSeed = false; | 357 | fHaveSeed = false; |
| 358 | fSeedEncryptedLocked = false; | ||
| 328 | fDeriveComplete = true; | 359 | fDeriveComplete = true; |
| 329 | nSchema = HD_SCHEMA_NONE; | 360 | nSchema = HD_SCHEMA_NONE; |
| 330 | nDerivedKnown = 0; | 361 | nDerivedKnown = 0; |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 211e1e2..dd001c8 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -111,10 +111,16 @@ static void PrintUsage() | |||
| 111 | printf(" /help, -help, --help, /?\n"); | 111 | printf(" /help, -help, --help, /?\n"); |
| 112 | printf(" /datadir=PATH\n"); | 112 | printf(" /datadir=PATH\n"); |
| 113 | printf(" /debug\n"); | 113 | printf(" /debug\n"); |
| 114 | printf(" /debugrelay (/debug plus per-message relay chatter and block dumps)\n"); | ||
| 115 | printf(" /maxdebugfilesize=N (roll debug.log at N MB, default 1024; 0 = never)\n"); | ||
| 114 | printf(" /gen\n"); | 116 | printf(" /gen\n"); |
| 115 | printf(" /nogui or /daemon\n"); | 117 | printf(" /nogui or /daemon\n"); |
| 116 | printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); | 118 | printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); |
| 117 | printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); | 119 | printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n"); |
| 120 | printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n"); | ||
| 121 | printf(" net-message,\n"); | ||
| 122 | printf(" consensus-limits, pool-stratum,\n"); | ||
| 123 | printf(" parse-money, socks5-proxy, or managed-tor\n"); | ||
| 118 | printf("\n"); | 124 | printf("\n"); |
| 119 | printf("Mining mode:\n"); | 125 | printf("Mining mode:\n"); |
| 120 | printf(" /operator\n"); | 126 | printf(" /operator\n"); |
| @@ -175,6 +181,26 @@ static void PrintUsage() | |||
| 175 | printf(" /recoveryaudit (show how much spendable balance is\n"); | 181 | printf(" /recoveryaudit (show how much spendable balance is\n"); |
| 176 | printf(" covered by the recovery phrase;\n"); | 182 | printf(" covered by the recovery phrase;\n"); |
| 177 | printf(" exits 2 when wallet.dat is still needed)\n"); | 183 | printf(" exits 2 when wallet.dat is still needed)\n"); |
| 184 | printf(" /walletstorageaudit (count wallet.dat record types without\n"); | ||
| 185 | printf(" printing wallet values, then exit)\n"); | ||
| 186 | printf(" /walletstorageauditjson=FILE\n"); | ||
| 187 | printf(" (write the same audit as deterministic JSON)\n"); | ||
| 188 | printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); | ||
| 189 | printf(" wallet.dat storage state)\n"); | ||
| 190 | printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); | ||
| 191 | printf(" SQLite store, then exit)\n"); | ||
| 192 | printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n"); | ||
| 193 | printf(" printing counts only, then exit)\n"); | ||
| 194 | printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n"); | ||
| 195 | printf(" an empty data directory, then exit)\n"); | ||
| 196 | printf(" /walletsqliteloadcheck=FILE\n"); | ||
| 197 | printf(" (parse a SQLite export like the wallet loader)\n"); | ||
| 198 | printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n"); | ||
| 199 | printf(" /walletbackend=sqlite (opt-in: run the node on <datadir>/wallet.sqlite\n"); | ||
| 200 | printf(" instead of wallet.dat; export one first)\n"); | ||
| 201 | printf(" /createwallet (headless only: permit creating a new wallet in a\n"); | ||
| 202 | printf(" datadir that has none -- otherwise refused, since\n"); | ||
| 203 | printf(" a missing wallet is usually a mount/backup fault)\n"); | ||
| 178 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 204 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 179 | printf(" but never recorded, then exit)\n"); | 205 | printf(" but never recorded, then exit)\n"); |
| 180 | printf("\n"); | 206 | printf("\n"); |
| @@ -191,6 +217,54 @@ static void ParseStartupArguments(int argc, char* argv[]) | |||
| 191 | if (arg(argc,argv,"/debug") || arg(argc,argv,"-debug")) | 217 | if (arg(argc,argv,"/debug") || arg(argc,argv,"-debug")) |
| 192 | fDebug = true; | 218 | fDebug = true; |
| 193 | 219 | ||
| 220 | // /debugrelay -- everything /debug shows, plus the per-message firehose | ||
| 221 | // ("relay" and "nostrdiscovery" categories: sending: tx, Relaying wtx, | ||
| 222 | // raw CBlock dumps, per-peer discovery lines). That traffic was ~90% of | ||
| 223 | // a 13-hour production debug.log (issue #35), so plain /debug leaves it | ||
| 224 | // off; the message text itself is unchanged for anyone grepping it. | ||
| 225 | if (arg(argc,argv,"/debugrelay") || arg(argc,argv,"-debugrelay")) | ||
| 226 | { | ||
| 227 | fDebug = true; | ||
| 228 | fDebugRelay = true; | ||
| 229 | } | ||
| 230 | |||
| 231 | // /maxdebugfilesize=N -- roll debug.log to debug.log.old once it passes | ||
| 232 | // N MB, checked on each write. Default is 1024 (see util.cpp); 0 turns | ||
| 233 | // the check off and leaves rotation entirely to logrotate. | ||
| 234 | string strMaxDebugSize = argval2(argc, argv, "/maxdebugfilesize", "-maxdebugfilesize"); | ||
| 235 | if (!strMaxDebugSize.empty()) | ||
| 236 | { | ||
| 237 | // Parsed with atoi64() before, which is strtoll() with the endptr | ||
| 238 | // thrown away: it returns 0 for anything it cannot read, so | ||
| 239 | // /maxdebugfilesize=1G and =1024MB both came out as 0 -- "never | ||
| 240 | // roll", identical to asking for that on purpose, with nothing said | ||
| 241 | // to the operator. The old code also multiplied without a range | ||
| 242 | // check, so a big enough N wrapped to a negative cap, which the roll | ||
| 243 | // reads as "off" too. Every rejection is now reported and leaves the | ||
| 244 | // default in place; the value is a plain count of MB, no suffix. | ||
| 245 | const char* psz = strMaxDebugSize.c_str(); | ||
| 246 | char* pszEnd = NULL; | ||
| 247 | errno = 0; | ||
| 248 | int64 nMB = strtoll(psz, &pszEnd, 10); | ||
| 249 | // INT64_MAX, without depending on which limits header this tree pulls in. | ||
| 250 | const int64 nMaxMB = (int64)((~(uint64)0) >> 1) / (1024 * 1024); | ||
| 251 | const char* pszWhy = NULL; | ||
| 252 | if (pszEnd == psz) | ||
| 253 | pszWhy = "not a number"; | ||
| 254 | else if (*pszEnd != '\0') | ||
| 255 | pszWhy = "trailing characters -- the value is a count of MB, with no unit suffix"; | ||
| 256 | else if (errno == ERANGE || nMB < 0) | ||
| 257 | pszWhy = "not a size in MB"; | ||
| 258 | else if (nMB > nMaxMB) | ||
| 259 | pszWhy = "larger than a file size can be"; | ||
| 260 | if (pszWhy) | ||
| 261 | fprintf(stderr, "Ignoring /maxdebugfilesize=%s: %s (keeping %lld MB)\n", | ||
| 262 | strMaxDebugSize.c_str(), pszWhy, | ||
| 263 | (long long)(nMaxDebugFileSize / 1024 / 1024)); | ||
| 264 | else | ||
| 265 | nMaxDebugFileSize = nMB * 1024 * 1024; | ||
| 266 | } | ||
| 267 | |||
| 194 | if (arg(argc,argv,"/gen") || arg(argc,argv,"-gen")) | 268 | if (arg(argc,argv,"/gen") || arg(argc,argv,"-gen")) |
| 195 | { | 269 | { |
| 196 | fGenerateBitcoins = 1; | 270 | fGenerateBitcoins = 1; |
| @@ -530,10 +604,119 @@ int main(int argc, char* argv[]) | |||
| 530 | 604 | ||
| 531 | ParseStartupArguments(argc, argv); | 605 | ParseStartupArguments(argc, argv); |
| 532 | 606 | ||
| 607 | #ifdef _WIN32 | ||
| 608 | // Set by the GUI "Restart Now" button: give the previous instance a moment | ||
| 609 | // to release the network port and the block-index files before this one | ||
| 610 | // opens them, so a relaunch does not race the process it is replacing. | ||
| 611 | if (arg(argc, argv, "/restartwait") || arg(argc, argv, "-restartwait")) | ||
| 612 | Sleep(2500); | ||
| 613 | #endif | ||
| 614 | |||
| 615 | // Hidden self-test helper. It must run before LoadWallet(): several | ||
| 616 | // storage-sanity scenarios deliberately make wallet.dat unsafe to load. | ||
| 617 | string strSelfTestMutateWallet = | ||
| 618 | argval2(argc, argv, "/selftestmutatewallet", "-selftestmutatewallet"); | ||
| 619 | if (!strSelfTestMutateWallet.empty()) | ||
| 620 | return RunSelfTestMutateWallet(strSelfTestMutateWallet); | ||
| 621 | |||
| 533 | string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); | 622 | string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); |
| 534 | if (!strSelfTest.empty()) | 623 | if (!strSelfTest.empty()) |
| 535 | return RunSelfTest(strSelfTest); | 624 | return RunSelfTest(strSelfTest); |
| 536 | 625 | ||
| 626 | // Storage diagnostics must run before LoadWallet(). Their job is to | ||
| 627 | // explain wallet.dat states that the normal wallet loader may refuse. | ||
| 628 | string strWalletStorageAuditJson = | ||
| 629 | argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); | ||
| 630 | bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") || | ||
| 631 | arg(argc,argv,"-walletstorageaudit") || | ||
| 632 | !strWalletStorageAuditJson.empty(); | ||
| 633 | bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") || | ||
| 634 | arg(argc,argv,"-walletstoragecheck"); | ||
| 635 | |||
| 636 | // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite | ||
| 637 | // the live wallet is wallet.sqlite, so open it read-only as the active | ||
| 638 | // store and let the same scan walk the right file. This runs before the | ||
| 639 | // normal backend resolution further down, so validate the value here too. | ||
| 640 | string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend"); | ||
| 641 | if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() && | ||
| 642 | strDiagBackend != "sqlite" && strDiagBackend != "bdb") | ||
| 643 | { | ||
| 644 | AttachTerminal(); | ||
| 645 | fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite " | ||
| 646 | "or -walletbackend=bdb.\n", strDiagBackend.c_str()); | ||
| 647 | return 1; | ||
| 648 | } | ||
| 649 | bool fDiagSQLite = (strDiagBackend == "sqlite") && | ||
| 650 | (fDiagWantsAudit || fDiagWantsCheck); | ||
| 651 | if (fDiagSQLite) | ||
| 652 | { | ||
| 653 | AttachTerminal(); | ||
| 654 | string strSQLitePath = GetAppDir() + "/wallet.sqlite"; | ||
| 655 | string strOpenErr; | ||
| 656 | if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr)) | ||
| 657 | { | ||
| 658 | fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n", | ||
| 659 | strOpenErr.c_str()); | ||
| 660 | return 1; | ||
| 661 | } | ||
| 662 | } | ||
| 663 | |||
| 664 | if (fDiagWantsAudit) | ||
| 665 | { | ||
| 666 | int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); | ||
| 667 | if (fDiagSQLite) | ||
| 668 | WalletSQLiteRuntimeClose(); | ||
| 669 | else | ||
| 670 | DBFlush(true); | ||
| 671 | return nRet; | ||
| 672 | } | ||
| 673 | |||
| 674 | if (fDiagWantsCheck) | ||
| 675 | { | ||
| 676 | int nRet = CmdWalletStorageCheck(); | ||
| 677 | if (fDiagSQLite) | ||
| 678 | WalletSQLiteRuntimeClose(); | ||
| 679 | else | ||
| 680 | DBFlush(true); | ||
| 681 | return nRet; | ||
| 682 | } | ||
| 683 | |||
| 684 | string strWalletSQLiteExport = | ||
| 685 | argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport"); | ||
| 686 | if (!strWalletSQLiteExport.empty()) | ||
| 687 | { | ||
| 688 | int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport); | ||
| 689 | DBFlush(true); | ||
| 690 | return nRet; | ||
| 691 | } | ||
| 692 | |||
| 693 | string strWalletSQLiteVerify = | ||
| 694 | argval2(argc, argv, "/walletsqliteverify", "-walletsqliteverify"); | ||
| 695 | if (!strWalletSQLiteVerify.empty()) | ||
| 696 | { | ||
| 697 | int nRet = CmdWalletSQLiteVerify(strWalletSQLiteVerify); | ||
| 698 | DBFlush(true); | ||
| 699 | return nRet; | ||
| 700 | } | ||
| 701 | |||
| 702 | string strWalletSQLiteRestore = | ||
| 703 | argval2(argc, argv, "/walletsqliterestore", "-walletsqliterestore"); | ||
| 704 | if (!strWalletSQLiteRestore.empty()) | ||
| 705 | { | ||
| 706 | int nRet = CmdWalletSQLiteRestore(strWalletSQLiteRestore); | ||
| 707 | DBFlush(true); | ||
| 708 | return nRet; | ||
| 709 | } | ||
| 710 | |||
| 711 | string strWalletSQLiteLoadCheck = | ||
| 712 | argval2(argc, argv, "/walletsqliteloadcheck", "-walletsqliteloadcheck"); | ||
| 713 | if (!strWalletSQLiteLoadCheck.empty()) | ||
| 714 | { | ||
| 715 | int nRet = CmdWalletSQLiteLoadCheck(strWalletSQLiteLoadCheck); | ||
| 716 | DBFlush(true); | ||
| 717 | return nRet; | ||
| 718 | } | ||
| 719 | |||
| 537 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it | 720 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it |
| 538 | // through. Nothing on this path caught anything, so an unreadable | 721 | // through. Nothing on this path caught anything, so an unreadable |
| 539 | // blkindex.dat or wallet.dat -- one written by another platform's Berkeley | 722 | // blkindex.dat or wallet.dat -- one written by another platform's Berkeley |
| @@ -578,18 +761,197 @@ int main(int argc, char* argv[]) | |||
| 578 | printf("Loading wallet...\n"); | 761 | printf("Loading wallet...\n"); |
| 579 | try | 762 | try |
| 580 | { | 763 | { |
| 581 | if (!LoadWallet()) | 764 | string strWalletBackend = |
| 765 | argval2(argc, argv, "/walletbackend", "-walletbackend"); | ||
| 766 | string strWalletSQLite = | ||
| 767 | argval2(argc, argv, "/walletsqlite", "-walletsqlite"); | ||
| 768 | |||
| 769 | // A misspelled backend must not fall through to Berkeley DB silently -- | ||
| 770 | // someone who typed -walletbackend=sqlit meant to run on SQLite, and | ||
| 771 | // opening wallet.dat instead of telling them is the wrong surprise. | ||
| 772 | if (!strWalletBackend.empty() && | ||
| 773 | strWalletBackend != "sqlite" && strWalletBackend != "bdb") | ||
| 582 | { | 774 | { |
| 583 | // LoadWallet() explains itself into strWalletLoadError when it | 775 | AttachTerminal(); |
| 584 | // knows why -- an unsupported wallet format, for one, which is the | 776 | FatalStartupError(fHeadlessStartup, |
| 585 | // case this whole path exists to make visible. | 777 | strprintf("Unknown wallet backend '%s'.", strWalletBackend.c_str()), |
| 586 | FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", | 778 | "Use -walletbackend=sqlite for the SQLite backend, or " |
| 587 | strWalletLoadError); | 779 | "-walletbackend=bdb for the Berkeley DB wallet.dat."); |
| 588 | return 1; | 780 | return 1; |
| 589 | } | 781 | } |
| 590 | 782 | ||
| 783 | if (strWalletBackend == "sqlite") | ||
| 784 | { | ||
| 785 | // Explicit opt-in via the flag. The node runs on | ||
| 786 | // <datadir>/wallet.sqlite and never opens wallet.dat, but the user | ||
| 787 | // has to have exported one first -- there is no silent migration, | ||
| 788 | // and wallet.dat is left | ||
| 789 | // exactly where it is so a plain restart goes back to it. | ||
| 790 | AttachTerminal(); | ||
| 791 | string strSQLitePath = GetAppDir() + "/wallet.sqlite"; | ||
| 792 | if (!FileExists(strSQLitePath.c_str())) | ||
| 793 | { | ||
| 794 | string strGuide = strprintf( | ||
| 795 | "No SQLite wallet was found at\n" | ||
| 796 | " %s\n\n" | ||
| 797 | "The SQLite wallet backend is opt-in and needs an exported\n" | ||
| 798 | "wallet first. Your Berkeley DB wallet.dat is not touched by\n" | ||
| 799 | "any of this.\n\n" | ||
| 800 | " Step 1 Export your existing wallet to SQLite:\n" | ||
| 801 | " bitflash -walletsqliteexport=\"%s\"\n" | ||
| 802 | " Step 2 Start again with the SQLite backend:\n" | ||
| 803 | " bitflash -walletbackend=sqlite\n\n" | ||
| 804 | "To go back to Berkeley DB at any time, just start without\n" | ||
| 805 | "-walletbackend=sqlite. wallet.dat is still your wallet.", | ||
| 806 | strSQLitePath.c_str(), strSQLitePath.c_str()); | ||
| 807 | FatalStartupError(fHeadlessStartup, | ||
| 808 | "The SQLite wallet backend needs an exported wallet first.", | ||
| 809 | strGuide); | ||
| 810 | return 1; | ||
| 811 | } | ||
| 812 | |||
| 813 | if (!LoadWalletFromSQLiteRuntime(strSQLitePath)) | ||
| 814 | { | ||
| 815 | FatalStartupError(fHeadlessStartup, | ||
| 816 | "Cannot open the SQLite wallet backend.", | ||
| 817 | strWalletLoadError); | ||
| 818 | return 1; | ||
| 819 | } | ||
| 820 | |||
| 821 | fprintf(stderr, | ||
| 822 | "Wallet backend: SQLite\n" | ||
| 823 | " %s\n" | ||
| 824 | "Your Berkeley DB wallet.dat is left untouched. Restart without\n" | ||
| 825 | "-walletbackend=sqlite to go back to it.\n", | ||
| 826 | strSQLitePath.c_str()); | ||
| 827 | fflush(stderr); | ||
| 828 | // Fall through and run the node normally: every CWalletDB read, | ||
| 829 | // write, and erase now routes to this SQLite file. | ||
| 830 | } | ||
| 831 | else if (!strWalletSQLite.empty()) | ||
| 832 | { | ||
| 833 | AttachTerminal(); | ||
| 834 | if (!LoadWalletFromSQLite(strWalletSQLite)) | ||
| 835 | { | ||
| 836 | FatalStartupError(fHeadlessStartup, | ||
| 837 | "Cannot open SQLite wallet export.", | ||
| 838 | strWalletLoadError); | ||
| 839 | return 1; | ||
| 840 | } | ||
| 841 | |||
| 842 | fprintf(stderr, "SQLite wallet loaded: read-only staging backend\n"); | ||
| 843 | fprintf(stderr, "SQLite runtime writes are not enabled yet; exiting before " | ||
| 844 | "network, mining, GUI, or wallet mutation starts.\n"); | ||
| 845 | fflush(stderr); | ||
| 846 | DBFlush(true); | ||
| 847 | return 0; | ||
| 848 | } | ||
| 849 | else if (strWalletBackend == "bdb") | ||
| 850 | { | ||
| 851 | // Explicit -walletbackend=bdb overrides the marker and the default: | ||
| 852 | // force Berkeley DB (LoadWallet creates wallet.dat if none exists). | ||
| 853 | if (!LoadWallet()) | ||
| 854 | { | ||
| 855 | FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", | ||
| 856 | strWalletLoadError); | ||
| 857 | return 1; | ||
| 858 | } | ||
| 859 | } | ||
| 860 | else | ||
| 861 | { | ||
| 862 | // No explicit -walletbackend on the command line. Resolve it from | ||
| 863 | // the persistent marker, then defaults: an existing wallet.sqlite | ||
| 864 | // stays on SQLite, an existing wallet.dat stays on Berkeley DB (the | ||
| 865 | // GUI offers to convert), and a fresh install starts on SQLite -- | ||
| 866 | // the new default. wallet.dat is never touched by any of this. | ||
| 867 | string strSQLitePath = GetAppDir() + "/wallet.sqlite"; | ||
| 868 | bool fSQLiteExists = FileExists(strSQLitePath.c_str()); | ||
| 869 | bool fWalletDatExists = FileExists((GetAppDir() + "/wallet.dat").c_str()); | ||
| 870 | string strMarker = ReadWalletBackendMarker(); | ||
| 871 | |||
| 872 | bool fUseSQLite = false; | ||
| 873 | bool fCreateSQLite = false; | ||
| 874 | if (strMarker == "sqlite") | ||
| 875 | { | ||
| 876 | if (fSQLiteExists) fUseSQLite = true; | ||
| 877 | else if (fWalletDatExists) fUseSQLite = false; // marker stale; real wallet is BDB | ||
| 878 | else { fUseSQLite = true; fCreateSQLite = true; } | ||
| 879 | } | ||
| 880 | else if (strMarker == "bdb") | ||
| 881 | { | ||
| 882 | fUseSQLite = false; | ||
| 883 | } | ||
| 884 | else // no marker recorded yet | ||
| 885 | { | ||
| 886 | if (fSQLiteExists && !fWalletDatExists) fUseSQLite = true; | ||
| 887 | else if (fWalletDatExists) fUseSQLite = false; | ||
| 888 | else { fUseSQLite = true; fCreateSQLite = true; } | ||
| 889 | } | ||
| 890 | |||
| 891 | // A headless node whose datadir holds no wallet of either kind is | ||
| 892 | // far more often an operations fault -- datadir not mounted, or | ||
| 893 | // wallet.dat caught mid-rename by a backup -- than a first run. | ||
| 894 | // Creating one here is not the harmless default it looks like: | ||
| 895 | // success also writes the backend marker, and from then on | ||
| 896 | // restoring the real wallet.dat no longer takes effect, because the | ||
| 897 | // marker keeps the node on the empty wallet it just made. Under | ||
| 898 | // 1.2.19 the same fault created a wallet.dat that a restore | ||
| 899 | // overwrote, so operators could recover by putting the file back. | ||
| 900 | // Refuse instead, and name the flag that makes it deliberate. | ||
| 901 | if (fCreateSQLite && fHeadlessStartup && | ||
| 902 | !arg(argc, argv, "/createwallet") && !arg(argc, argv, "-createwallet")) | ||
| 903 | { | ||
| 904 | AttachTerminal(); | ||
| 905 | fprintf(stderr, | ||
| 906 | "No wallet found in %s\n" | ||
| 907 | "Refusing to create one. On a headless node an absent wallet is\n" | ||
| 908 | "usually a mount or backup fault, and creating one would record a\n" | ||
| 909 | "backend marker that makes restoring the real wallet a no-op.\n" | ||
| 910 | "Pass -createwallet to start a new one deliberately, or\n" | ||
| 911 | "-walletbackend=bdb to run on a Berkeley DB wallet.dat.\n", | ||
| 912 | GetAppDir().c_str()); | ||
| 913 | fflush(stderr); | ||
| 914 | return 1; | ||
| 915 | } | ||
| 916 | |||
| 917 | if (fUseSQLite) | ||
| 918 | { | ||
| 919 | AttachTerminal(); | ||
| 920 | bool fOk = fCreateSQLite | ||
| 921 | ? CreateNewSQLiteWallet(strSQLitePath) | ||
| 922 | : LoadWalletFromSQLiteRuntime(strSQLitePath); | ||
| 923 | if (!fOk) | ||
| 924 | { | ||
| 925 | FatalStartupError(fHeadlessStartup, | ||
| 926 | fCreateSQLite ? "Cannot create the SQLite wallet." | ||
| 927 | : "Cannot open the SQLite wallet.", | ||
| 928 | strWalletLoadError); | ||
| 929 | return 1; | ||
| 930 | } | ||
| 931 | // Record the choice so it is stable from here on. | ||
| 932 | if (ReadWalletBackendMarker() != "sqlite") | ||
| 933 | WriteWalletBackendMarker("sqlite"); | ||
| 934 | fprintf(stderr, "Wallet backend: SQLite\n %s\n%s", | ||
| 935 | strSQLitePath.c_str(), | ||
| 936 | fCreateSQLite ? "A new wallet was created here.\n" : ""); | ||
| 937 | fflush(stderr); | ||
| 938 | // Fall through and run the node; CWalletDB routes to this file. | ||
| 939 | } | ||
| 940 | else if (!LoadWallet()) | ||
| 941 | { | ||
| 942 | // LoadWallet() explains itself into strWalletLoadError when it | ||
| 943 | // knows why -- an unsupported wallet format, for one, which is | ||
| 944 | // the case this whole path exists to make visible. | ||
| 945 | FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", | ||
| 946 | strWalletLoadError); | ||
| 947 | return 1; | ||
| 948 | } | ||
| 949 | } | ||
| 950 | |||
| 591 | // After the block index, so there is a chain to compare the wallet | 951 | // After the block index, so there is a chain to compare the wallet |
| 592 | // against, and before anything reports a balance. | 952 | // against, and before anything reports a balance. Works on either |
| 953 | // backend: RescanSpentFlags writes corrected flags through CWalletDB, | ||
| 954 | // which routes to SQLite when that backend is active. | ||
| 593 | RescanSpentFlags(); | 955 | RescanSpentFlags(); |
| 594 | } | 956 | } |
| 595 | catch (const std::exception& e) | 957 | catch (const std::exception& e) |
| @@ -858,6 +1220,10 @@ int main(int argc, char* argv[]) | |||
| 858 | // called here, which is why a wallet.dat copied elsewhere would not | 1220 | // called here, which is why a wallet.dat copied elsewhere would not |
| 859 | // open -- issue #40. Not optional. | 1221 | // open -- issue #40. Not optional. |
| 860 | DBFlush(true); | 1222 | DBFlush(true); |
| 1223 | // Checkpoint and close the SQLite wallet backend if one is active. A | ||
| 1224 | // no-op on the Berkeley DB path. Safe to kill before this runs: WAL + | ||
| 1225 | // synchronous=FULL already made every committed write durable. | ||
| 1226 | WalletSQLiteRuntimeClose(); | ||
| 861 | #ifdef _WIN32 | 1227 | #ifdef _WIN32 |
| 862 | // wallet.dat is safely flushed and closed now; let a pending console | 1228 | // wallet.dat is safely flushed and closed now; let a pending console |
| 863 | // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish. | 1229 | // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish. |
| @@ -874,6 +1240,7 @@ int main(int argc, char* argv[]) | |||
| 874 | StopNode(); | 1240 | StopNode(); |
| 875 | BtfStopManagedTor(); | 1241 | BtfStopManagedTor(); |
| 876 | DBFlush(true); | 1242 | DBFlush(true); |
| 1243 | WalletSQLiteRuntimeClose(); | ||
| 877 | return ret; | 1244 | return ret; |
| 878 | #endif | 1245 | #endif |
| 879 | } | 1246 | } |
diff --git a/src/makefile.mingw b/src/makefile.mingw index 0ba1d76..740bba6 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -24,7 +24,7 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ | |||
| 24 | 24 | ||
| 25 | LIBS = -L$(DEPS)/lib -lsecp256k1 \ | 25 | LIBS = -L$(DEPS)/lib -lsecp256k1 \ |
| 26 | -L$(RANDOMX)/build -lrandomx \ | 26 | -L$(RANDOMX)/build -lrandomx \ |
| 27 | -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ | 27 | -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ |
| 28 | -LC:/msys64/ucrt64/lib \ | 28 | -LC:/msys64/ucrt64/lib \ |
| 29 | -lglfw3 -lopengl32 -lgdi32 \ | 29 | -lglfw3 -lopengl32 -lgdi32 \ |
| 30 | -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ | 30 | -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ |
| @@ -37,7 +37,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | |||
| 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 38 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 38 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 39 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ | 39 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 40 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o | 40 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ |
| 41 | obj/proxy.o obj/tor.o | ||
| 41 | 42 | ||
| 42 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 43 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o |
| 43 | 44 | ||
| @@ -51,7 +52,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | |||
| 51 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 52 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 52 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ | 53 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ |
| 53 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ | 54 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ |
| 54 | selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h | 55 | selftest.h walletcmd.h wallet_sqlite.h sockcount.h proxy.h tor.h compat.h font_roboto.h |
| 55 | 56 | ||
| 56 | all: bitflash.exe | 57 | all: bitflash.exe |
| 57 | 58 | ||
| @@ -107,8 +108,13 @@ tests: test_bip32.exe bitflash.exe | |||
| 107 | ./bitflash.exe -selftest=wallet-keypool | 108 | ./bitflash.exe -selftest=wallet-keypool |
| 108 | ./bitflash.exe -selftest=wallet-hd | 109 | ./bitflash.exe -selftest=wallet-hd |
| 109 | ./bitflash.exe -selftest=wallet-format | 110 | ./bitflash.exe -selftest=wallet-format |
| 111 | ./bitflash.exe -selftest=wallet-sqlite | ||
| 112 | ./bitflash.exe -selftest=wallet-sqlite-migration | ||
| 110 | ./bitflash.exe -selftest=wallet-crypto | 113 | ./bitflash.exe -selftest=wallet-crypto |
| 111 | ./bitflash.exe -selftest=wallet-encrypt | 114 | ./bitflash.exe -selftest=wallet-encrypt |
| 115 | ./bitflash.exe -selftest=wallet-sqlite-encrypt | ||
| 116 | ./bitflash.exe -selftest=wallet-backend-default | ||
| 117 | ./bitflash.exe -selftest=wallet-convert | ||
| 112 | ./bitflash.exe -selftest=net-message | 118 | ./bitflash.exe -selftest=net-message |
| 113 | ./bitflash.exe -selftest=consensus-limits | 119 | ./bitflash.exe -selftest=consensus-limits |
| 114 | ./bitflash.exe -selftest=pool-stratum | 120 | ./bitflash.exe -selftest=pool-stratum |
diff --git a/src/net.cpp b/src/net.cpp index fed6123..bb05259 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -1560,12 +1560,41 @@ void ThreadSocketHandler2(void* parg) | |||
| 1560 | unsigned int nNodes = 0; | 1560 | unsigned int nNodes = 0; |
| 1561 | CRITICAL_BLOCK(cs_vNodes) | 1561 | CRITICAL_BLOCK(cs_vNodes) |
| 1562 | nNodes = (unsigned int)vNodes.size(); | 1562 | nNodes = (unsigned int)vNodes.size(); |
| 1563 | // Also count how many connections this one address already | ||
| 1564 | // holds. A single IP that keeps opening sockets -- a broken | ||
| 1565 | // node or a deliberate connection-exhaustion flood -- must not | ||
| 1566 | // be able to take the whole table, which is what starved the | ||
| 1567 | // relays. | ||
| 1568 | // | ||
| 1569 | // Loopback is exempt. Onion peers reach a managed-Tor node | ||
| 1570 | // through its local hidden-service listener, so every one of | ||
| 1571 | // them accepts as 127.0.0.1; capping loopback would cap all | ||
| 1572 | // inbound onion peers as a single group, which is the opposite | ||
| 1573 | // of where the network is headed. The global MAX_CONNECTIONS | ||
| 1574 | // still bounds the total, and the direct-IP flood this defends | ||
| 1575 | // against never arrives over loopback. (Per-onion-identity | ||
| 1576 | // limiting, once a peer's identity is known past accept(), is | ||
| 1577 | // the right tool for onion and is left for later.) | ||
| 1578 | bool fLoopback = (addr.GetByte(3) == 127); | ||
| 1579 | unsigned int nFromThisIP = 0; | ||
| 1580 | if (!fLoopback) | ||
| 1581 | CRITICAL_BLOCK(cs_vNodes) | ||
| 1582 | for (vector<CNode*>::iterator it = vNodes.begin(); it != vNodes.end(); ++it) | ||
| 1583 | if ((*it)->addr.ip == addr.ip) | ||
| 1584 | nFromThisIP++; | ||
| 1585 | |||
| 1563 | if (nNodes >= MAX_CONNECTIONS) | 1586 | if (nNodes >= MAX_CONNECTIONS) |
| 1564 | { | 1587 | { |
| 1565 | LogPrint("net", "refusing connection from %s, already at %u\n", | 1588 | LogPrint("net", "refusing connection from %s, already at %u\n", |
| 1566 | addr.ToString().c_str(), nNodes); | 1589 | addr.ToString().c_str(), nNodes); |
| 1567 | BtfCloseSocket(hSocket); | 1590 | BtfCloseSocket(hSocket); |
| 1568 | } | 1591 | } |
| 1592 | else if (!fLoopback && nFromThisIP >= MAX_CONNECTIONS_PER_IP) | ||
| 1593 | { | ||
| 1594 | LogPrint("net", "refusing connection from %s, already %u from that address\n", | ||
| 1595 | addr.ToString().c_str(), nFromThisIP); | ||
| 1596 | BtfCloseSocket(hSocket); | ||
| 1597 | } | ||
| 1569 | else | 1598 | else |
| 1570 | { | 1599 | { |
| 1571 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); | 1600 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); |
| @@ -2090,7 +2119,7 @@ bool StartNode(string& strError) | |||
| 2090 | if (addrIncoming.ip) | 2119 | if (addrIncoming.ip) |
| 2091 | addrLocalHost.ip = addrIncoming.ip; | 2120 | addrLocalHost.ip = addrIncoming.ip; |
| 2092 | 2121 | ||
| 2093 | _beginthread([](void*) { | 2122 | if (_beginthread([](void*) { |
| 2094 | unsigned int ip = addrLocalHost.ip; | 2123 | unsigned int ip = addrLocalHost.ip; |
| 2095 | if (GetMyExternalIP(ip)) { | 2124 | if (GetMyExternalIP(ip)) { |
| 2096 | addrLocalHost.ip = ip; | 2125 | addrLocalHost.ip = ip; |
| @@ -2098,7 +2127,8 @@ bool StartNode(string& strError) | |||
| 2098 | CWalletDB().WriteSetting("addrIncoming", addrIncoming); | 2127 | CWalletDB().WriteSetting("addrIncoming", addrIncoming); |
| 2099 | printf("External IP updated: %s\n", addrLocalHost.ToStringIP().c_str()); | 2128 | printf("External IP updated: %s\n", addrLocalHost.ToStringIP().c_str()); |
| 2100 | } | 2129 | } |
| 2101 | }, 0, NULL); | 2130 | }, 0, NULL) == (uintptr_t)-1) |
| 2131 | printf("Error: _beginthread(GetMyExternalIP) failed\n"); | ||
| 2102 | 2132 | ||
| 2103 | // Peers that answered last time, dialled straight away. The relays are the | 2133 | // Peers that answered last time, dialled straight away. The relays are the |
| 2104 | // only way to find anyone otherwise, and reaching them plus walking their | 2134 | // only way to find anyone otherwise, and reaching them plus walking their |
| @@ -23,6 +23,12 @@ enum | |||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | 25 | ||
| 26 | // Defined in main.cpp. True while this node is tracking the tip rather than | ||
| 27 | // still catching up, measured off the age of our own best block -- see there | ||
| 28 | // for why that and not a peer-reported height. Declared here because net.h is | ||
| 29 | // parsed before main.h and CNode::AskFor needs it. | ||
| 30 | bool IsBlockFetchNearTip(); | ||
| 31 | |||
| 26 | bool GetMyExternalIP(unsigned int& ipRet); | 32 | bool GetMyExternalIP(unsigned int& ipRet); |
| 27 | CNode* ConnectNodeBtf(const string& strBtfAddr); | 33 | CNode* ConnectNodeBtf(const string& strBtfAddr); |
| 28 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, | 34 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, |
| @@ -89,6 +95,15 @@ static const int BTF_RECV_TIMEOUT_SECS = 30 * 60; | |||
| 89 | static const int BTF_SEND_STALL_SECS = 10 * 60; | 95 | static const int BTF_SEND_STALL_SECS = 10 * 60; |
| 90 | static const int BTF_HANDSHAKE_GRACE_SECS = 60; | 96 | static const int BTF_HANDSHAKE_GRACE_SECS = 60; |
| 91 | 97 | ||
| 98 | // How long a requested block gets before it is asked for again, in seconds. | ||
| 99 | // Used twice: CNode::AskFor spaces the queued per-announcer block retries this | ||
| 100 | // far apart instead of the two minutes transactions get, and the failover | ||
| 101 | // sweep in main.cpp re-issues a getdata to a different peer once a sent | ||
| 102 | // request has gone unanswered this long. A block body is at most 1 MB and | ||
| 103 | // somebody demonstrably has it -- they announced it -- so on this timescale | ||
| 104 | // "not here yet" means "not coming from that peer", not "still in transit". | ||
| 105 | static const int BLOCK_FETCH_RETRY_SECS = 15; | ||
| 106 | |||
| 92 | // A serialized object cannot be larger than MAX_SIZE, so no valid network | 107 | // A serialized object cannot be larger than MAX_SIZE, so no valid network |
| 93 | // message needs the old 256 MB header allowance. Keep the wire cap aligned with | 108 | // message needs the old 256 MB header allowance. Keep the wire cap aligned with |
| 94 | // what the deserializer can accept. How long a peer may sit on a partial | 109 | // what the deserializer can accept. How long a peer may sit on a partial |
| @@ -109,6 +124,23 @@ static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; | |||
| 109 | // 125 is Bitcoin's number and remains a conservative public-node default. | 124 | // 125 is Bitcoin's number and remains a conservative public-node default. |
| 110 | static const unsigned int MAX_CONNECTIONS = 125; | 125 | static const unsigned int MAX_CONNECTIONS = 125; |
| 111 | 126 | ||
| 127 | // Cap inbound connections from any single address. Without it one source can | ||
| 128 | // hold every slot up to MAX_CONNECTIONS -- exactly what happened to the | ||
| 129 | // rendezvous relays, where a single IP parked hundreds of connections and | ||
| 130 | // starved real peers. A legitimate peer, even a relay talking to a relay, | ||
| 131 | // needs only a handful; 32 leaves generous room while denying any one address | ||
| 132 | // more than a quarter of the table. The firewall connlimit on the relays does | ||
| 133 | // the same thing a layer lower; this makes every build carry the defense. | ||
| 134 | // | ||
| 135 | // Loopback (127.0.0.0/8) is exempt in the accept path. A managed-Tor node | ||
| 136 | // receives every inbound onion peer over its local hidden-service listener, so | ||
| 137 | // they all appear as 127.0.0.1 -- capping loopback would cap onion peers as a | ||
| 138 | // group, which is exactly wrong for a network moving toward onion-only. The | ||
| 139 | // global MAX_CONNECTIONS still bounds the total; a per-onion-identity limit, | ||
| 140 | // applied once identity is known after accept(), is the right future tool for | ||
| 141 | // onion floods. | ||
| 142 | static const unsigned int MAX_CONNECTIONS_PER_IP = 32; | ||
| 143 | |||
| 112 | // A node marked for disconnect is normally held until its buffers drain, so | 144 | // A node marked for disconnect is normally held until its buffers drain, so |
| 113 | // a last message still goes out. But vSend cannot drain through a socket | 145 | // a last message still goes out. But vSend cannot drain through a socket |
| 114 | // whose far end is gone, and that made such a node immortal: it stayed in | 146 | // whose far end is gone, and that made such a node immortal: it stayed in |
| @@ -795,13 +827,51 @@ public: | |||
| 795 | static int64 nLastTime; | 827 | static int64 nLastTime; |
| 796 | nLastTime = nNow = max(nNow, ++nLastTime); | 828 | nLastTime = nNow = max(nNow, ++nLastTime); |
| 797 | 829 | ||
| 798 | // Each retry is 2 minutes after the last | 830 | // Each retry is 2 minutes after the last -- and because nRequestTime |
| 799 | nRequestTime = max(nRequestTime + 2 * 60 * 1000000, nNow); | 831 | // is shared across peers, the same spacing is what strings requests |
| 832 | // for one inv across its announcers. For transactions that is pacing; | ||
| 833 | // for a new block it was the stall of issue #33: nine peers announce | ||
| 834 | // the block, the first one asked does not serve it, and the ninth | ||
| 835 | // candidate is not even asked until sixteen minutes in, with the pool | ||
| 836 | // mining on the stale tip the whole time. | ||
| 837 | // | ||
| 838 | // Blocks retry on the fetch timescale, but only while we are at the | ||
| 839 | // tip. That is where the stall costs something -- the pool keeps | ||
| 840 | // handing out jobs against a dead tip -- and it is also where the | ||
| 841 | // spacing is nearly free: one block is in flight, and its body has | ||
| 842 | // almost always arrived before the second announcer's turn comes | ||
| 843 | // round, so AlreadyHave drops the duplicate before a getdata is sent. | ||
| 844 | // | ||
| 845 | // While catching up neither half holds. A peer working through a | ||
| 846 | // 500-deep queue can be minutes away from block N without anything | ||
| 847 | // being wrong, so at 15s the second announcer is asked at +15s, the | ||
| 848 | // third at +30s, and the bulk download arrives once per announcer -- | ||
| 849 | // up to eight times over. Nothing caps that: the eight-entry cap is on | ||
| 850 | // mapBlockFetch, which is a different mechanism from this spacing. | ||
| 851 | // Two minutes is the right number there, and it is what this line did | ||
| 852 | // before. Failover at the tip does not depend on this being 15s in any | ||
| 853 | // case: mapBlockFetch re-issues a stalled getdata to the next peer on | ||
| 854 | // its own timer, which is what actually fixes #33. | ||
| 855 | bool fFast = (inv.type == MSG_BLOCK && IsBlockFetchNearTip()); | ||
| 856 | int64 nRetryUsecs = (fFast ? BLOCK_FETCH_RETRY_SECS : 2 * 60) * 1000000LL; | ||
| 857 | nRequestTime = max(nRequestTime + nRetryUsecs, nNow); | ||
| 800 | mapAskFor.insert(make_pair(nRequestTime, inv)); | 858 | mapAskFor.insert(make_pair(nRequestTime, inv)); |
| 801 | } | 859 | } |
| 802 | 860 | ||
| 803 | 861 | ||
| 804 | 862 | ||
| 863 | // "sending: tx (N bytes)" alone was ~71% of a 13-hour production | ||
| 864 | // debug.log (issue #35), so tx sends are demoted to the /debugrelay-gated | ||
| 865 | // "relay" category; every other command's send trace stays on "net". | ||
| 866 | // The line is emitted in pieces across BeginMessage/EndMessage/ | ||
| 867 | // AbortMessage, so all three must agree on the category or orphaned | ||
| 868 | // "(N bytes)" fragments would leak into a plain -debug log. | ||
| 869 | // Valid only mid-message (nPushPos != -1). | ||
| 870 | const char* SendLogCategory() const | ||
| 871 | { | ||
| 872 | return strcmp(GetMessageCommand(), "tx") == 0 ? "relay" : "net"; | ||
| 873 | } | ||
| 874 | |||
| 805 | void BeginMessage(const char* pszCommand) | 875 | void BeginMessage(const char* pszCommand) |
| 806 | { | 876 | { |
| 807 | EnterCriticalSection(&cs_vSend); | 877 | EnterCriticalSection(&cs_vSend); |
| @@ -809,17 +879,19 @@ public: | |||
| 809 | AbortMessage(); | 879 | AbortMessage(); |
| 810 | nPushPos = vSend.size(); | 880 | nPushPos = vSend.size(); |
| 811 | vSend << CMessageHeader(pszCommand, 0); | 881 | vSend << CMessageHeader(pszCommand, 0); |
| 812 | if (LogAcceptsCategory("net")) printf("sending: %-12s ", pszCommand); | 882 | if (LogAcceptsCategory(SendLogCategory())) printf("sending: %-12s ", pszCommand); |
| 813 | } | 883 | } |
| 814 | 884 | ||
| 815 | void AbortMessage() | 885 | void AbortMessage() |
| 816 | { | 886 | { |
| 817 | if (nPushPos == -1) | 887 | if (nPushPos == -1) |
| 818 | return; | 888 | return; |
| 889 | // Capture before nPushPos is cleared; GetMessageCommand() needs it. | ||
| 890 | const char* pszCategory = SendLogCategory(); | ||
| 819 | vSend.resize(nPushPos); | 891 | vSend.resize(nPushPos); |
| 820 | nPushPos = -1; | 892 | nPushPos = -1; |
| 821 | LeaveCriticalSection(&cs_vSend); | 893 | LeaveCriticalSection(&cs_vSend); |
| 822 | if (LogAcceptsCategory("net")) printf("(aborted)\n"); | 894 | if (LogAcceptsCategory(pszCategory)) printf("(aborted)\n"); |
| 823 | } | 895 | } |
| 824 | 896 | ||
| 825 | void EndMessage() | 897 | void EndMessage() |
| @@ -827,7 +899,11 @@ public: | |||
| 827 | extern int nDropMessagesTest; | 899 | extern int nDropMessagesTest; |
| 828 | if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) | 900 | if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) |
| 829 | { | 901 | { |
| 830 | if (LogAcceptsCategory("net")) printf("dropmessages DROPPING SEND MESSAGE\n"); | 902 | // Same category as the "sending: " this line interrupts, so a tx |
| 903 | // does not announce its own drop under "net" having been gated | ||
| 904 | // off under "relay". nPushPos can be -1 here, and then | ||
| 905 | // GetMessageCommand() returns "" and this is "net" as before. | ||
| 906 | if (LogAcceptsCategory(SendLogCategory())) printf("dropmessages DROPPING SEND MESSAGE\n"); | ||
| 831 | AbortMessage(); | 907 | AbortMessage(); |
| 832 | return; | 908 | return; |
| 833 | } | 909 | } |
| @@ -839,10 +915,10 @@ public: | |||
| 839 | unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader); | 915 | unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader); |
| 840 | memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize)); | 916 | memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize)); |
| 841 | 917 | ||
| 842 | if (LogAcceptsCategory("net")) printf("(%d bytes) ", nSize); | 918 | if (LogAcceptsCategory(SendLogCategory())) printf("(%d bytes) ", nSize); |
| 843 | //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++) | 919 | //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++) |
| 844 | // printf("%02x ", vSend[i] & 0xff); | 920 | // printf("%02x ", vSend[i] & 0xff); |
| 845 | if (LogAcceptsCategory("net")) printf("\n"); | 921 | if (LogAcceptsCategory(SendLogCategory())) printf("\n"); |
| 846 | 922 | ||
| 847 | nPushPos = -1; | 923 | nPushPos = -1; |
| 848 | LeaveCriticalSection(&cs_vSend); | 924 | LeaveCriticalSection(&cs_vSend); |
diff --git a/src/nostr.cpp b/src/nostr.cpp index e2d839c..f44dc08 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -151,6 +151,19 @@ string BtfActiveRelay() | |||
| 151 | 151 | ||
| 152 | void BtfSetActiveRelay(const string& relay) | 152 | void BtfSetActiveRelay(const string& relay) |
| 153 | { | 153 | { |
| 154 | // With /rvrelay set, the descriptor must never name anything but the pin. | ||
| 155 | // No caller can reach here with another relay today -- both registration | ||
| 156 | // paths draw their list from BtfAllRelays(), which returns the pin alone -- | ||
| 157 | // but the descriptor is the one thing a miner obeys, so the invariant is | ||
| 158 | // enforced at the sink instead of being retrusted to every future caller. | ||
| 159 | if (fBtfRelayPinned && | ||
| 160 | find(vBtfMeetingRelays.begin(), vBtfMeetingRelays.end(), relay) == vBtfMeetingRelays.end()) | ||
| 161 | { | ||
| 162 | LogPrint("pool", "refusing to advertise meeting relay %s: /rvrelay pins %s\n", | ||
| 163 | relay.c_str(), | ||
| 164 | vBtfMeetingRelays.empty() ? "an empty list" : vBtfMeetingRelays[0].c_str()); | ||
| 165 | return; | ||
| 166 | } | ||
| 154 | CRITICAL_BLOCK(cs_activeRelay) | 167 | CRITICAL_BLOCK(cs_activeRelay) |
| 155 | strBtfActiveRelay = relay; | 168 | strBtfActiveRelay = relay; |
| 156 | } | 169 | } |
| @@ -223,6 +236,18 @@ vector<string> BtfAllRelays() | |||
| 223 | return all; | 236 | return all; |
| 224 | } | 237 | } |
| 225 | 238 | ||
| 239 | // Relays learned from Nostr, whether or not a pin is in force. Reporting only: | ||
| 240 | // it exists so the pool can name what /rvrelay is excluding. Anything choosing | ||
| 241 | // a relay to register or dial at must keep using BtfAllRelays(). | ||
| 242 | vector<string> BtfDiscoveredRelays() | ||
| 243 | { | ||
| 244 | vector<string> out; | ||
| 245 | CRITICAL_BLOCK(cs_discoveredRelays) | ||
| 246 | foreach(const string& r, g_discoveredRelays) | ||
| 247 | out.push_back(r); | ||
| 248 | return out; | ||
| 249 | } | ||
| 250 | |||
| 226 | 251 | ||
| 227 | // | 252 | // |
| 228 | // Utilities | 253 | // Utilities |
| @@ -1440,7 +1465,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 1440 | else if (createdAt > it->second) it->second = createdAt; // refresh | 1465 | else if (createdAt > it->second) it->second = createdAt; // refresh |
| 1441 | } | 1466 | } |
| 1442 | if (fNew && fDebug) | 1467 | if (fNew && fDebug) |
| 1443 | LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); | 1468 | LogPrint("nostrdiscovery", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); |
| 1444 | if (++nPeers > 500) break; | 1469 | if (++nPeers > 500) break; |
| 1445 | } | 1470 | } |
| 1446 | else if (type == "EOSE") | 1471 | else if (type == "EOSE") |
diff --git a/src/nostr.h b/src/nostr.h index 6dbbe26..0dd919d 100644 --- a/src/nostr.h +++ b/src/nostr.h | |||
| @@ -60,6 +60,9 @@ extern bool fBtfRelayPinned; | |||
| 60 | bool LooksLikeRelay(const std::string& r); | 60 | bool LooksLikeRelay(const std::string& r); |
| 61 | // Every relay to try: curated seeds + relays discovered from Nostr announcements. | 61 | // Every relay to try: curated seeds + relays discovered from Nostr announcements. |
| 62 | std::vector<std::string> BtfAllRelays(); | 62 | std::vector<std::string> BtfAllRelays(); |
| 63 | // Relays discovered from Nostr, ignoring the pin. For reporting what a pin | ||
| 64 | // excludes -- never for choosing a relay to register or dial at. | ||
| 65 | std::vector<std::string> BtfDiscoveredRelays(); | ||
| 63 | 66 | ||
| 64 | struct BtfPoolAnnouncement | 67 | struct BtfPoolAnnouncement |
| 65 | { | 68 | { |
diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 1d058d4..9c59364 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | #include <thread> | 8 | #include <thread> |
| 9 | #include <vector> | 9 | #include <vector> |
| 10 | #include <mutex> | 10 | #include <mutex> |
| 11 | #include <system_error> | ||
| 11 | 12 | ||
| 12 | // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it | 13 | // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it |
| 13 | // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal | 14 | // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal |
| @@ -101,13 +102,28 @@ bool RandomXInitDataset(int nThreads) | |||
| 101 | 102 | ||
| 102 | std::vector<std::thread> workers; | 103 | std::vector<std::thread> workers; |
| 103 | unsigned long per = total / nThreads; | 104 | unsigned long per = total / nThreads; |
| 104 | for (int i = 0; i < nThreads; i++) | 105 | try |
| 105 | { | 106 | { |
| 106 | unsigned long start = i * per; | 107 | for (int i = 0; i < nThreads; i++) |
| 107 | unsigned long count = (i == nThreads - 1) ? (total - start) : per; | 108 | { |
| 108 | workers.emplace_back([start, count]() { | 109 | unsigned long start = i * per; |
| 109 | randomx_init_dataset(g_dataset, g_cache, start, count); | 110 | unsigned long count = (i == nThreads - 1) ? (total - start) : per; |
| 110 | }); | 111 | workers.emplace_back([start, count]() { |
| 112 | randomx_init_dataset(g_dataset, g_cache, start, count); | ||
| 113 | }); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | catch (const std::system_error& e) | ||
| 117 | { | ||
| 118 | // Same refusal as the allocation failure above: light mode is slow | ||
| 119 | // but alive, std::terminate is neither. Join the workers that did | ||
| 120 | // start before releasing the dataset out from under them. | ||
| 121 | for (auto& w : workers) w.join(); | ||
| 122 | randomx_release_dataset(g_dataset); | ||
| 123 | g_dataset = NULL; | ||
| 124 | printf("RandomX: cannot spawn dataset init threads (%s), staying in light mode\n", | ||
| 125 | e.what()); | ||
| 126 | return false; | ||
| 111 | } | 127 | } |
| 112 | for (auto& w : workers) w.join(); | 128 | for (auto& w : workers) w.join(); |
| 113 | 129 | ||
diff --git a/src/readme.txt b/src/readme.txt index 752be66..7490a9a 100644 --- a/src/readme.txt +++ b/src/readme.txt | |||
| @@ -16,7 +16,7 @@ This product includes software developed by the OpenSSL Project. | |||
| 16 | Differences from the original Bitcoin | 16 | Differences from the original Bitcoin |
| 17 | ------------------------------------- | 17 | ------------------------------------- |
| 18 | - Own genesis block (22/Jul/2026 headline, independent chain) | 18 | - Own genesis block (22/Jul/2026 headline, independent chain) |
| 19 | - Own magic bytes (0xbf 0x1a 0x5c 0xfd) and P2P port 8433 | 19 | - Own magic bytes (0xbf 0x20 0x5c 0xfd) and P2P port 8433 |
| 20 | - Addresses start with "B" (ADDRESSVERSION = 25) | 20 | - Addresses start with "B" (ADDRESSVERSION = 25) |
| 21 | - Consensus fixes applied: value overflow (CVE-2010-5139), duplicate inputs, | 21 | - Consensus fixes applied: value overflow (CVE-2010-5139), duplicate inputs, |
| 22 | money range checks (MoneyRange) | 22 | money range checks (MoneyRange) |
diff --git a/src/rpc.cpp b/src/rpc.cpp index a4b53ca..3827cd5 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -115,15 +115,28 @@ static uint256 ShareTargetFromDifficulty(double difficulty) | |||
| 115 | { | 115 | { |
| 116 | if (difficulty <= 0.0) difficulty = 1.0; | 116 | if (difficulty <= 0.0) difficulty = 1.0; |
| 117 | static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); | 117 | static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); |
| 118 | // Representability floor. shareTarget = diff1 / difficulty, and diff1 is | ||
| 119 | // ~uint256(0) >> 12, i.e. a hair under 2^244 -- so the true target stops | ||
| 120 | // fitting in a uint256 once difficulty reaches 2^-12 (= 1/4096), and the | ||
| 121 | // fixed-point divide below breaks even earlier in a second way: the | ||
| 122 | // (uint64)(difficulty * 65536.0) divisor truncates to zero for any | ||
| 123 | // difficulty under 2^-16. Both thresholds sit far below VARDIFF_MIN | ||
| 124 | // (0.001), so live operation never comes near this, but a caller that | ||
| 125 | // bypasses vardiff (or a future VARDIFF_MIN) must not divide by zero or | ||
| 126 | // hand back getuint256()'s low-256-bit truncation of an oversized | ||
| 127 | // quotient -- a wrapped target is not merely wrong, it is arbitrarily | ||
| 128 | // HARDER than difficulty 1. At these difficulties every hash is meant to | ||
| 129 | // be a share, so the honest answer is the easiest representable target. | ||
| 130 | // (This replaces a dead branch that read `if (t < diff1 && difficulty < | ||
| 131 | // 1.0)`: t = diff1/difficulty, so difficulty < 1.0 implies t > diff1 and | ||
| 132 | // the two conditions could never both hold.) | ||
| 133 | if (difficulty < 1.0 / 4096.0) return ~uint256(0); | ||
| 118 | // shareTarget = diff1 / difficulty | 134 | // shareTarget = diff1 / difficulty |
| 119 | CBigNum bn; bn.setuint256(diff1); | 135 | CBigNum bn; bn.setuint256(diff1); |
| 120 | // Multiply by 2^16 to keep precision, divide, shift back | 136 | // Multiply by 2^16 to keep precision, divide, shift back |
| 121 | bn <<= 16; | 137 | bn <<= 16; |
| 122 | bn /= (uint64)(difficulty * 65536.0); | 138 | bn /= (uint64)(difficulty * 65536.0); |
| 123 | uint256 t = bn.getuint256(); | 139 | return bn.getuint256(); |
| 124 | // Cap at all-ones (maximum, easiest possible) | ||
| 125 | if (t < diff1 && difficulty < 1.0) t = ~uint256(0); | ||
| 126 | return t; | ||
| 127 | } | 140 | } |
| 128 | 141 | ||
| 129 | // Expected number of hashes to find one share at a given Stratum difficulty, | 142 | // Expected number of hashes to find one share at a given Stratum difficulty, |
| @@ -223,6 +236,29 @@ static bool RebuildJob() | |||
| 223 | // From the pool, so the template's payout key is in wallet.dat before | 236 | // From the pool, so the template's payout key is in wallet.dat before |
| 224 | // any miner is handed work against it. | 237 | // any miner is handed work against it. |
| 225 | vector<unsigned char> vchPubKey = GetKeyFromPool(); | 238 | vector<unsigned char> vchPubKey = GetKeyFromPool(); |
| 239 | if (vchPubKey.empty()) { | ||
| 240 | // Empty means the wallet could not produce a key: encrypted and | ||
| 241 | // locked, or key generation itself failed. Serialised, an empty | ||
| 242 | // vector is a zero-length push, so the coinbase would become | ||
| 243 | // `<empty> OP_CHECKSIG` -- a script nobody can satisfy. Every | ||
| 244 | // block mined against such a job burns its whole subsidy, while | ||
| 245 | // QueuePayouts still records the full obligation and the miners | ||
| 246 | // are then paid out of coin that block never contributed. | ||
| 247 | // BitcoinMiner refuses to mine in this state; refuse to hand the | ||
| 248 | // pool's miners work in it too. printf and not LogPrint for the | ||
| 249 | // reason given above the deaf-node refusal, and throttled the | ||
| 250 | // same way -- the no-job watchdog retries every few seconds for | ||
| 251 | // as long as the condition lasts. | ||
| 252 | static int64 nLastSaidNoKey = 0; | ||
| 253 | if (GetTime() - nLastSaidNoKey > 60) { | ||
| 254 | nLastSaidNoKey = GetTime(); | ||
| 255 | printf("RebuildJob: no wallet key available for the coinbase " | ||
| 256 | "(wallet locked, or key generation failed -- the " | ||
| 257 | "GetKeyFromPool line above says which) -- not issuing " | ||
| 258 | "jobs\n"); | ||
| 259 | } | ||
| 260 | return false; | ||
| 261 | } | ||
| 226 | static std::atomic<uint32_t> sExtra{0}; | 262 | static std::atomic<uint32_t> sExtra{0}; |
| 227 | uint32_t extraNonce = ++sExtra; | 263 | uint32_t extraNonce = ++sExtra; |
| 228 | 264 | ||
| @@ -267,16 +303,29 @@ static bool RebuildJob() | |||
| 267 | map<uint256, CTxIndex> pool; | 303 | map<uint256, CTxIndex> pool; |
| 268 | vector<char> vfAlreadyAdded(mapTransactions.size()); | 304 | vector<char> vfAlreadyAdded(mapTransactions.size()); |
| 269 | bool fFoundSomething = true; | 305 | bool fFoundSomething = true; |
| 270 | unsigned int sz = 0; | 306 | // The cap is MAX_BLOCK_SIZE_GEN, not MAX_SIZE/2. MAX_SIZE is |
| 271 | // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the | 307 | // the 32 MB serialisation ceiling, so half of it is 16 MB -- |
| 272 | // 32 MB serialisation ceiling, so half of it is 16 MB -- this | 308 | // this builder would happily assemble a template that its own |
| 273 | // builder would happily assemble a template that its own | ||
| 274 | // CheckBlock() rejects at the 1 MB consensus cap. This is the | 309 | // CheckBlock() rejects at the 1 MB consensus cap. This is the |
| 275 | // pool's job builder, so a block over the cap is not one | 310 | // pool's job builder, so a block over the cap is not one |
| 276 | // wasted solve, it is every miner on the pool working a | 311 | // wasted solve, it is every miner on the pool working a |
| 277 | // template the network will reject. The inner break bounds the | 312 | // template the network will reject. |
| 278 | // overshoot to a single transaction within a pass. | 313 | // |
| 279 | while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2) | 314 | // The inner break used to bound the overshoot to one |
| 315 | // transaction per pass. One transaction is still too many: | ||
| 316 | // nothing caps how big a single transaction may be, so that | ||
| 317 | // bound was worth up to MAX_SIZE. Asking whether each one fits | ||
| 318 | // before appending it bounds the overshoot at zero instead. | ||
| 319 | // | ||
| 320 | // Seeded with the header, tx count and coinbase, because | ||
| 321 | // CheckBlock() weighs the whole serialized block. | ||
| 322 | unsigned int sz = ::GetSerializeSize(block, SER_DISK); | ||
| 323 | // And the sigop budget, which CheckBlock() enforces and this | ||
| 324 | // builder never counted against. Same counting function | ||
| 325 | // CheckBlock() sums, seeded from the coinbase because it sums | ||
| 326 | // over all of vtx. | ||
| 327 | unsigned int nSigOps = block.vtx[0].GetSigOpCount(); | ||
| 328 | while (fFoundSomething && sz < MAX_BLOCK_SIZE_GEN) | ||
| 280 | { | 329 | { |
| 281 | fFoundSomething = false; | 330 | fFoundSomething = false; |
| 282 | unsigned int n = 0; | 331 | unsigned int n = 0; |
| @@ -287,16 +336,26 @@ static bool RebuildJob() | |||
| 287 | continue; | 336 | continue; |
| 288 | CTransaction& tx = (*mi).second; | 337 | CTransaction& tx = (*mi).second; |
| 289 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; | 338 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; |
| 339 | // Skip rather than stop: one transaction too big for | ||
| 340 | // the room left does not mean the next is. Ordered | ||
| 341 | // before ConnectInputs because ConnectInputs credits | ||
| 342 | // nFees by reference on success, and a transaction we | ||
| 343 | // then decline would leave the coinbase claiming a fee | ||
| 344 | // this block does not collect. | ||
| 345 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 346 | unsigned int nTxSigOps = tx.GetSigOpCount(); | ||
| 347 | if (!BlockTemplateFits(sz, nSigOps, nTxSize, nTxSigOps)) | ||
| 348 | continue; | ||
| 290 | map<uint256, CTxIndex> tmp(pool); | 349 | map<uint256, CTxIndex> tmp(pool); |
| 291 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, | 350 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, |
| 292 | false, true, tx.GetMinFee(block.vtx.size() < 100))) | 351 | false, true, tx.GetMinFee(block.vtx.size() < 100))) |
| 293 | continue; | 352 | continue; |
| 294 | swap(pool, tmp); | 353 | swap(pool, tmp); |
| 295 | block.vtx.push_back(tx); | 354 | block.vtx.push_back(tx); |
| 296 | sz += ::GetSerializeSize(tx, SER_NETWORK); | 355 | sz += nTxSize; |
| 356 | nSigOps += nTxSigOps; | ||
| 297 | vfAlreadyAdded[n] = true; | 357 | vfAlreadyAdded[n] = true; |
| 298 | fFoundSomething = true; | 358 | fFoundSomething = true; |
| 299 | if (sz > MAX_BLOCK_SIZE / 2) break; | ||
| 300 | } | 359 | } |
| 301 | } | 360 | } |
| 302 | } | 361 | } |
| @@ -776,6 +835,29 @@ static void PublishPendingPayouts(const VersionedBlob& v) | |||
| 776 | gPayoutLedgerPublishedSeq, "pending payout ledger"); | 835 | gPayoutLedgerPublishedSeq, "pending payout ledger"); |
| 777 | } | 836 | } |
| 778 | 837 | ||
| 838 | // Parse a persisted block-hash field, refusing to guess. SetHex() reads | ||
| 839 | // whatever hex prefix it finds and stops at the first character it does not | ||
| 840 | // recognise, so a corrupt value does not fail to parse -- it parses as a | ||
| 841 | // DIFFERENT hash. Both shapes of that are wrong in a dangerous direction: a | ||
| 842 | // value with a hex-looking prefix ("deadbeef...") becomes a hash no block | ||
| 843 | // ever had, which PayoutBlockStillOnChain reads as an orphaned block and | ||
| 844 | // FlushMaturePayouts then VOIDS the debt; a value with no hex prefix at all | ||
| 845 | // becomes 0, which is grandfathered past the orphan check on the strength | ||
| 846 | // of garbage. A field that is not the 64-digit hex string ToString() wrote | ||
| 847 | // is corruption, and corruption is a load failure like any other parse | ||
| 848 | // error -- the file is quarantined, not read around. | ||
| 849 | static uint256 ParsePayoutBlockHash(const std::string& str) | ||
| 850 | { | ||
| 851 | if (str.size() != 64 || | ||
| 852 | str.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) | ||
| 853 | throw std::runtime_error( | ||
| 854 | strprintf("blockHash \"%s\" is not a 64-digit hex string", | ||
| 855 | str.c_str())); | ||
| 856 | uint256 h; | ||
| 857 | h.SetHex(str); | ||
| 858 | return h; | ||
| 859 | } | ||
| 860 | |||
| 779 | static void LoadPendingPayouts() | 861 | static void LoadPendingPayouts() |
| 780 | { | 862 | { |
| 781 | std::string path = PayoutFilePath(); | 863 | std::string path = PayoutFilePath(); |
| @@ -797,7 +879,8 @@ static void LoadPendingPayouts() | |||
| 797 | // field. Leave blockHash zero and they are grandfathered past the | 879 | // field. Leave blockHash zero and they are grandfathered past the |
| 798 | // orphan check rather than voided on a guess. | 880 | // orphan check rather than voided on a guess. |
| 799 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 881 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 800 | pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 882 | pp.blockHash = |
| 883 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 801 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 884 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 802 | pp.blockHeight = obj["blockHeight"].get<int>(); | 885 | pp.blockHeight = obj["blockHeight"].get<int>(); |
| 803 | // Upstream's tolerance for an entry with no usable height, kept | 886 | // Upstream's tolerance for an entry with no usable height, kept |
| @@ -976,7 +1059,8 @@ static void LoadPaying() | |||
| 976 | r.amount = obj["amount"].get<int64>(); | 1059 | r.amount = obj["amount"].get<int64>(); |
| 977 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); | 1060 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); |
| 978 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 1061 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 979 | r.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 1062 | r.blockHash = |
| 1063 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 980 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 1064 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 981 | r.blockHeight = obj["blockHeight"].get<int>(); | 1065 | r.blockHeight = obj["blockHeight"].get<int>(); |
| 982 | if (obj.contains("retries") && obj["retries"].is_number()) | 1066 | if (obj.contains("retries") && obj["retries"].is_number()) |
| @@ -1186,11 +1270,35 @@ static bool PayoutBlockStillOnChain(const uint256& blockHash) | |||
| 1186 | return false; | 1270 | return false; |
| 1187 | } | 1271 | } |
| 1188 | 1272 | ||
| 1189 | static std::string PayoutComment(int blockHeight) | 1273 | // The comment every pre-upgrade payout transaction carries, and therefore the |
| 1274 | // ONLY needle the legacy fallback in FindRecordedPoolPayout may search with. | ||
| 1275 | // This format is frozen: change it and every payment those builds made stops | ||
| 1276 | // matching, every one of those debts looks unpaid, and reconciliation pays | ||
| 1277 | // them all a second time. | ||
| 1278 | static std::string LegacyPayoutComment(int blockHeight) | ||
| 1190 | { | 1279 | { |
| 1191 | return strprintf("Pool payout height %d", blockHeight); | 1280 | return strprintf("Pool payout height %d", blockHeight); |
| 1192 | } | 1281 | } |
| 1193 | 1282 | ||
| 1283 | // The comment written on new sends. It names the funding block, because the | ||
| 1284 | // height alone cannot: a height that is won, lost to a reorg, and won again | ||
| 1285 | // yields two rounds whose height-only comments are identical -- and identical | ||
| 1286 | // amounts too, whenever the same miner held both rounds. The structured | ||
| 1287 | // pool_payout_* metadata already keys the payment by hash for the code; this | ||
| 1288 | // puts the same fact where an operator reading the wallet can see it, and | ||
| 1289 | // makes a new transaction unmatchable by the legacy needle even if the | ||
| 1290 | // structured keys are ever lost. | ||
| 1291 | // | ||
| 1292 | // A zero hash keeps the legacy form: those records predate hash keying, and | ||
| 1293 | // there is no hash to name. | ||
| 1294 | static std::string PayoutComment(int blockHeight, const uint256& blockHash) | ||
| 1295 | { | ||
| 1296 | if (blockHash == 0) | ||
| 1297 | return LegacyPayoutComment(blockHeight); | ||
| 1298 | return strprintf("Pool payout height %d block %s", blockHeight, | ||
| 1299 | blockHash.ToString().c_str()); | ||
| 1300 | } | ||
| 1301 | |||
| 1194 | // Startup reconciliation. | 1302 | // Startup reconciliation. |
| 1195 | // | 1303 | // |
| 1196 | // This used to be the crash-recovery mechanism: it scanned the wallet with | 1304 | // This used to be the crash-recovery mechanism: it scanned the wallet with |
| @@ -1345,14 +1453,37 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1345 | PublishPoolRounds(snap); | 1453 | PublishPoolRounds(snap); |
| 1346 | } | 1454 | } |
| 1347 | 1455 | ||
| 1456 | // Does this round have a published round proof? | ||
| 1457 | // | ||
| 1458 | // Existence answers a temporal question the legacy fallback below needs: | ||
| 1459 | // which builds could have paid this round. RecordPoolRound writes the proof | ||
| 1460 | // when the round is created, and structured payout metadata (upstream | ||
| 1461 | // cbdd06d) is an ancestor of round proofs (upstream 83ba822) -- no build | ||
| 1462 | // able to write a proof has ever sent a payment that only a comment can | ||
| 1463 | // identify. A round with a proof therefore cannot have a comment-only | ||
| 1464 | // payment; one that matches it is another round's, colliding on the height. | ||
| 1465 | static bool PoolRoundHasProof(const std::string& id) | ||
| 1466 | { | ||
| 1467 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 1468 | for (const PoolRoundProof& r : gPoolRounds) | ||
| 1469 | if (r.id == id) | ||
| 1470 | return true; | ||
| 1471 | return false; | ||
| 1472 | } | ||
| 1473 | |||
| 1348 | // Which wallet transaction, if any, already paid this? | 1474 | // Which wallet transaction, if any, already paid this? |
| 1349 | // | 1475 | // |
| 1350 | // Two kinds of evidence, in order of quality. | 1476 | // Two kinds of evidence, in order of quality. |
| 1351 | // | 1477 | // |
| 1352 | // Structured (pool_payout_id / pool_payout_address / pool_payout_amount) is | 1478 | // Structured (pool_payout_id / pool_payout_address / pool_payout_amount, |
| 1353 | // upstream's, and it is exact: the id is derived from the block hash, so it | 1479 | // plus pool_payout_block since this series) is upstream's, extended. The id |
| 1354 | // names one round and one round only. Every transaction this build sends | 1480 | // is derived from the block hash, but only from the FIRST sixteen hex digits |
| 1355 | // carries it. | 1481 | // of it -- and proof-of-work concentrates zeros exactly there, so the id can |
| 1482 | // name two same-height rounds identically. New sends therefore record the | ||
| 1483 | // full hash as pool_payout_block, and a structured match must agree with it | ||
| 1484 | // wherever both sides have one; a transaction without the key was sent | ||
| 1485 | // before it existed, and for those the id is the best evidence there is. | ||
| 1486 | // Every transaction this build sends carries all four keys. | ||
| 1356 | // | 1487 | // |
| 1357 | // But NO transaction this pool has ever sent carries it. Every payout in the | 1488 | // But NO transaction this pool has ever sent carries it. Every payout in the |
| 1358 | // existing wallet has only a "comment". So on the first boot after this | 1489 | // existing wallet has only a "comment". So on the first boot after this |
| @@ -1375,11 +1506,22 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1375 | // (round, address) pair -- from the ledger's paidTxids, which is durable, | 1506 | // (round, address) pair -- from the ledger's paidTxids, which is durable, |
| 1376 | // and from what this process has already claimed. One legacy transaction | 1507 | // and from what this process has already claimed. One legacy transaction |
| 1377 | // can therefore settle at most one debt, so N transactions can never | 1508 | // can therefore settle at most one debt, so N transactions can never |
| 1378 | // satisfy more than N obligations. | 1509 | // satisfy more than N obligations; |
| 1379 | // | 1510 | // |
| 1380 | // What is left unresolved by construction is two pre-upgrade rounds at the | 1511 | // 4. it is not consulted at all for a round that has a round proof |
| 1381 | // SAME height paying the same address the same amount, since the comment | 1512 | // (fAllowLegacy, decided by the caller from PoolRoundHasProof). Proofs |
| 1382 | // carries only the height. That needs a reorg. | 1513 | // are written when a round is created, and only by builds whose every |
| 1514 | // payment names its round exactly -- so a comment-only transaction | ||
| 1515 | // cannot be a payment for such a round, only a same-height collision | ||
| 1516 | // from an older one, and skipping the fallback cannot hide a real | ||
| 1517 | // payment: the structured probe above already found any that exist. | ||
| 1518 | // | ||
| 1519 | // What is left unresolved by construction is a comment-only transaction and | ||
| 1520 | // a PROOFLESS round at the same height paying the same address the same | ||
| 1521 | // amount: two grandfathered zero-hash twins, or a twin pair from the builds | ||
| 1522 | // that keyed the ledger by hash before any metadata reached the wallet. The | ||
| 1523 | // comment carries only the height, so nothing ties the transaction to one | ||
| 1524 | // twin rather than the other. That needs a reorg. | ||
| 1383 | // | 1525 | // |
| 1384 | // This used to say the losing twin's ledger entry is voided by the orphan | 1526 | // This used to say the losing twin's ledger entry is voided by the orphan |
| 1385 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() | 1527 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() |
| @@ -1413,8 +1555,10 @@ enum PayoutProbeResult { | |||
| 1413 | 1555 | ||
| 1414 | static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | 1556 | static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, |
| 1415 | int blockHeight, | 1557 | int blockHeight, |
| 1558 | const uint256& blockHash, | ||
| 1416 | const std::string& address, | 1559 | const std::string& address, |
| 1417 | int64 amount, | 1560 | int64 amount, |
| 1561 | bool fAllowLegacy, | ||
| 1418 | const std::set<std::string>& excluded, | 1562 | const std::set<std::string>& excluded, |
| 1419 | std::string& txidRet) | 1563 | std::string& txidRet) |
| 1420 | { | 1564 | { |
| @@ -1425,7 +1569,7 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1425 | CScript want; | 1569 | CScript want; |
| 1426 | if (fHaveScript) | 1570 | if (fHaveScript) |
| 1427 | want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; | 1571 | want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; |
| 1428 | const std::string comment = PayoutComment(blockHeight); | 1572 | const std::string comment = LegacyPayoutComment(blockHeight); |
| 1429 | 1573 | ||
| 1430 | // Candidate legacy matches, adjudicated after cs_mapWallet is released. | 1574 | // Candidate legacy matches, adjudicated after cs_mapWallet is released. |
| 1431 | // | 1575 | // |
| @@ -1458,10 +1602,25 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1458 | map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); | 1602 | map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); |
| 1459 | if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) | 1603 | if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) |
| 1460 | continue; | 1604 | continue; |
| 1605 | // The id alone cannot tell twin rounds at one height apart | ||
| 1606 | // (see above); the full hash can, where the transaction | ||
| 1607 | // recorded one. Where it did not, the id decides, as it | ||
| 1608 | // always has -- refusing those transactions would make every | ||
| 1609 | // payment sent before the key existed look unpaid and pay it | ||
| 1610 | // again. | ||
| 1611 | if (blockHash != 0) { | ||
| 1612 | map<string, string>::const_iterator blkIt = | ||
| 1613 | wtx.mapValue.find("pool_payout_block"); | ||
| 1614 | if (blkIt != wtx.mapValue.end() | ||
| 1615 | && blkIt->second != blockHash.ToString()) | ||
| 1616 | continue; // a twin round's payment, not this round's | ||
| 1617 | } | ||
| 1461 | txidRet = wtx.GetHash().GetHex(); | 1618 | txidRet = wtx.GetHash().GetHex(); |
| 1462 | return PAYOUT_PROBE_STRUCTURED; | 1619 | return PAYOUT_PROBE_STRUCTURED; |
| 1463 | } | 1620 | } |
| 1464 | 1621 | ||
| 1622 | if (!fAllowLegacy) | ||
| 1623 | continue; // comment-only evidence is inadmissible: rule 4 | ||
| 1465 | if (!fHaveScript) | 1624 | if (!fHaveScript) |
| 1466 | continue; | 1625 | continue; |
| 1467 | // The comment is written only by the payout path, so its presence | 1626 | // The comment is written only by the payout path, so its presence |
| @@ -1732,7 +1891,9 @@ static void FlushMaturePayouts() | |||
| 1732 | } | 1891 | } |
| 1733 | int64 total = 0; | 1892 | int64 total = 0; |
| 1734 | for (auto& kv : it->amounts) total += kv.second; | 1893 | for (auto& kv : it->amounts) total += kv.second; |
| 1735 | LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" | 1894 | // Always: one line per voided entry, never a collapsed summary. |
| 1895 | // A reorg voids these in a burst from this one source line. | ||
| 1896 | LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" | ||
| 1736 | " payment(s) totalling %s\n", | 1897 | " payment(s) totalling %s\n", |
| 1737 | it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), | 1898 | it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), |
| 1738 | it->amounts.size(), FormatMoney(total).c_str()); | 1899 | it->amounts.size(), FormatMoney(total).c_str()); |
| @@ -1931,7 +2092,7 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid) | |||
| 1931 | // disagreeing. | 2092 | // disagreeing. |
| 1932 | static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) | 2093 | static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) |
| 1933 | { | 2094 | { |
| 1934 | LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n", | 2095 | LogPrintAlways("payout", "[payout] writing off %s owed to %s for block %d: %s\n", |
| 1935 | FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, | 2096 | FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, |
| 1936 | reason.c_str()); | 2097 | reason.c_str()); |
| 1937 | VersionedBlob blob; | 2098 | VersionedBlob blob; |
| @@ -2036,7 +2197,7 @@ static void PayoutThreadFn(void*) | |||
| 2036 | // funding block can land at any point in that window. Upstream has | 2197 | // funding block can land at any point in that window. Upstream has |
| 2037 | // no equivalent -- it decides once, when the entry matures. | 2198 | // no equivalent -- it decides once, when the entry matures. |
| 2038 | if (!PayoutBlockStillOnChain(r.blockHash)) { | 2199 | if (!PayoutBlockStillOnChain(r.blockHash)) { |
| 2039 | LogPrint("payout", "[payout] block %d (%s) orphaned while queued" | 2200 | LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued" |
| 2040 | " -- dropping %s to %s\n", | 2201 | " -- dropping %s to %s\n", |
| 2041 | r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), | 2202 | r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), |
| 2042 | FormatMoney(r.amount).c_str(), r.address.c_str()); | 2203 | FormatMoney(r.amount).c_str(), r.address.c_str()); |
| @@ -2080,7 +2241,7 @@ static void PayoutThreadFn(void*) | |||
| 2080 | const std::string reason = | 2241 | const std::string reason = |
| 2081 | strprintf("'%s' is not a valid address, so no transaction" | 2242 | strprintf("'%s' is not a valid address, so no transaction" |
| 2082 | " can pay it", r.address.c_str()); | 2243 | " can pay it", r.address.c_str()); |
| 2083 | LogPrint("payout", "[payout] SKIPPED bad address '%s' --" | 2244 | LogPrintAlways("payout", "[payout] SKIPPED bad address '%s' --" |
| 2084 | " %s owed for block %d cannot be paid\n", | 2245 | " %s owed for block %d cannot be paid\n", |
| 2085 | r.address.c_str(), FormatMoney(r.amount).c_str(), | 2246 | r.address.c_str(), FormatMoney(r.amount).c_str(), |
| 2086 | r.blockHeight); | 2247 | r.blockHeight); |
| @@ -2099,9 +2260,15 @@ static void PayoutThreadFn(void*) | |||
| 2099 | std::set<std::string> excluded; | 2260 | std::set<std::string> excluded; |
| 2100 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, | 2261 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, |
| 2101 | claimKey, excluded); | 2262 | claimKey, excluded); |
| 2263 | // A round with a published proof was created by a build whose | ||
| 2264 | // every payment names its round exactly, so only structured | ||
| 2265 | // evidence can settle it -- a comment match would be another | ||
| 2266 | // round's payment colliding on the height. | ||
| 2267 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 2102 | r.fProbeWallet = false; | 2268 | r.fProbeWallet = false; |
| 2103 | PayoutProbeResult probe = FindRecordedPoolPayout( | 2269 | PayoutProbeResult probe = FindRecordedPoolPayout( |
| 2104 | id, r.blockHeight, r.address, r.amount, excluded, recovered); | 2270 | id, r.blockHeight, r.blockHash, r.address, r.amount, |
| 2271 | fAllowLegacy, excluded, recovered); | ||
| 2105 | if (probe != PAYOUT_PROBE_NONE) { | 2272 | if (probe != PAYOUT_PROBE_NONE) { |
| 2106 | if (probe == PAYOUT_PROBE_LEGACY) { | 2273 | if (probe == PAYOUT_PROBE_LEGACY) { |
| 2107 | // Loud on purpose: this is the upgrade path, where the | 2274 | // Loud on purpose: this is the upgrade path, where the |
| @@ -2186,7 +2353,7 @@ static void PayoutThreadFn(void*) | |||
| 2186 | } | 2353 | } |
| 2187 | 2354 | ||
| 2188 | CWalletTx wtx; | 2355 | CWalletTx wtx; |
| 2189 | wtx.mapValue["comment"] = PayoutComment(r.blockHeight); | 2356 | wtx.mapValue["comment"] = PayoutComment(r.blockHeight, r.blockHash); |
| 2190 | // Upstream's structured metadata. This is what makes the wallet | 2357 | // Upstream's structured metadata. This is what makes the wallet |
| 2191 | // probe above possible at all -- the old comment-only form could | 2358 | // probe above possible at all -- the old comment-only form could |
| 2192 | // not distinguish two rounds paying the same address the same | 2359 | // not distinguish two rounds paying the same address the same |
| @@ -2195,6 +2362,11 @@ static void PayoutThreadFn(void*) | |||
| 2195 | wtx.mapValue["pool_payout_id"] = id; | 2362 | wtx.mapValue["pool_payout_id"] = id; |
| 2196 | wtx.mapValue["pool_payout_address"] = r.address; | 2363 | wtx.mapValue["pool_payout_address"] = r.address; |
| 2197 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); | 2364 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); |
| 2365 | // The full hash, not only the id's sixteen-digit prefix: the | ||
| 2366 | // prefix of a proof-of-work hash is where the zeros live, and | ||
| 2367 | // the probe needs to tell twin rounds at one height apart. | ||
| 2368 | if (r.blockHash != 0) | ||
| 2369 | wtx.mapValue["pool_payout_block"] = r.blockHash.ToString(); | ||
| 2198 | 2370 | ||
| 2199 | SendMoneyOutcome outcome; | 2371 | SendMoneyOutcome outcome; |
| 2200 | if (SendMoney(sc, r.amount, wtx, outcome)) { | 2372 | if (SendMoney(sc, r.amount, wtx, outcome)) { |
| @@ -2205,7 +2377,7 @@ static void PayoutThreadFn(void*) | |||
| 2205 | // described by at least one of them at every instant. | 2377 | // described by at least one of them at every instant. |
| 2206 | RecordPayoutTxid(r, wtx.GetHash().GetHex()); | 2378 | RecordPayoutTxid(r, wtx.GetHash().GetHex()); |
| 2207 | ForgetPayment(r); | 2379 | ForgetPayment(r); |
| 2208 | LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", | 2380 | LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n", |
| 2209 | FormatMoney(r.amount).c_str(), r.address.c_str(), | 2381 | FormatMoney(r.amount).c_str(), r.address.c_str(), |
| 2210 | wtx.GetHash().ToString().substr(0, 12).c_str()); | 2382 | wtx.GetHash().ToString().substr(0, 12).c_str()); |
| 2211 | } else { | 2383 | } else { |
| @@ -2263,7 +2435,7 @@ static void PayoutThreadFn(void*) | |||
| 2263 | // going away must not turn the log into one line a minute | 2435 | // going away must not turn the log into one line a minute |
| 2264 | // forever, and must not go quiet either. | 2436 | // forever, and must not go quiet either. |
| 2265 | if (r.retries <= 5 || r.retries % 60 == 0) | 2437 | if (r.retries <= 5 || r.retries % 60 == 0) |
| 2266 | LogPrint("payout", "[payout] FAILED to pay %s to %s" | 2438 | LogPrintAlways("payout", "[payout] FAILED to pay %s to %s" |
| 2267 | " (%s spendable, wallet unlocked, block %d on" | 2439 | " (%s spendable, wallet unlocked, block %d on" |
| 2268 | " the main chain) -- attempt %d, retrying in" | 2440 | " the main chain) -- attempt %d, retrying in" |
| 2269 | " 60s; this debt stands until it is paid\n", | 2441 | " 60s; this debt stands until it is paid\n", |
| @@ -2471,6 +2643,31 @@ static const double VARDIFF_MAX = 1e12; | |||
| 2471 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2643 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2472 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2644 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much |
| 2473 | 2645 | ||
| 2646 | // #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner | ||
| 2647 | // that stops hashing keeps its last estimate forever and RefreshStats sums | ||
| 2648 | // it into the announced pool total for as long as the connection sits open | ||
| 2649 | // -- the published pool hashrate ran several times over ground truth | ||
| 2650 | // because of exactly this. MinerReportedHashRate() below applies the | ||
| 2651 | // policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share | ||
| 2652 | // intervals (expected interval = expected hashes per share at its | ||
| 2653 | // difficulty, divided by the estimate itself) with no share, after which | ||
| 2654 | // the figure we report falls off with a half-life of one more such | ||
| 2655 | // interval. The interval is clamped to [HASHRATE_MIN_INTERVAL, | ||
| 2656 | // HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected | ||
| 2657 | // interval of milliseconds) is not punished for ordinary share-delivery | ||
| 2658 | // jitter, and a very slow miner's grace does not stretch past the point | ||
| 2659 | // where its estimate means anything. At the vardiff target of one share | ||
| 2660 | // per 15s that is a minute of grace, half the rate reported 75s in, and | ||
| 2661 | // nothing reported after about seventeen minutes of silence. | ||
| 2662 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | ||
| 2663 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | ||
| 2664 | static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval | ||
| 2665 | // Half-lives past the grace beyond which we report a flat zero. 2^-64 of the | ||
| 2666 | // original estimate is not a number anyone needs, and stopping here keeps | ||
| 2667 | // pow() from walking down into the denormals for a miner that has been quiet | ||
| 2668 | // for hours. | ||
| 2669 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2670 | |||
| 2474 | struct Miner { | 2671 | struct Miner { |
| 2475 | btf_socket_t fd; | 2672 | btf_socket_t fd; |
| 2476 | bool authorised = false; | 2673 | bool authorised = false; |
| @@ -2498,11 +2695,80 @@ struct Miner { | |||
| 2498 | int64 lastShareTime = 0; // GetTime() of the previous accepted share | 2695 | int64 lastShareTime = 0; // GetTime() of the previous accepted share |
| 2499 | double hashRate = 0.0; // smoothed H/s estimate | 2696 | double hashRate = 0.0; // smoothed H/s estimate |
| 2500 | 2697 | ||
| 2698 | // Replay guard for mining.submit (#8): nonces already credited on this | ||
| 2699 | // connection for the job it is currently working. Keyed to the job id in | ||
| 2700 | // seenNoncesJob and cleared by SendJob whenever that id changes, so a | ||
| 2701 | // resubmission under a NEW job is never turned away, while a replayed | ||
| 2702 | // (job_id, nonce) pair under the same job is refused before it can be | ||
| 2703 | // credited twice into roundShareCount -- the figure the payout split | ||
| 2704 | // divides by. | ||
| 2705 | std::set<uint32_t> seenNonces; | ||
| 2706 | std::string seenNoncesJob; | ||
| 2707 | |||
| 2501 | explicit Miner(btf_socket_t f) | 2708 | explicit Miner(btf_socket_t f) |
| 2502 | : fd(f), connectTime(GetTime()), | 2709 | : fd(f), connectTime(GetTime()), |
| 2503 | extranonce2(gNextExtranonce2.fetch_add(1)) {} | 2710 | extranonce2(gNextExtranonce2.fetch_add(1)) {} |
| 2504 | }; | 2711 | }; |
| 2505 | 2712 | ||
| 2713 | // What we publish as a miner's H/s: the last EWMA sample, faded out if shares | ||
| 2714 | // have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES). | ||
| 2715 | // | ||
| 2716 | // This reads m->hashRate and never writes it. That is the whole design. The | ||
| 2717 | // first attempt decayed the stored estimate in place from the event loop's | ||
| 2718 | // sweep, which put a running accumulator behind a figure that is only ever | ||
| 2719 | // displayed, and the accumulator misbehaved in two ways: | ||
| 2720 | // | ||
| 2721 | // - `expected` was recomputed from the already-decayed estimate on every | ||
| 2722 | // pass, so each decay raised `expected`, which raised `grace`, which is | ||
| 2723 | // the very thing gating the next decay. `grace` simply chased `silent` | ||
| 2724 | // upward and the reported rate settled into a 1/t crawl instead of the | ||
| 2725 | // half-life curve the comment above promises -- 291x too high at ten | ||
| 2726 | // half-lives past the grace. Issue #7 is about the pool over-reporting; | ||
| 2727 | // that under-corrected it for hours. | ||
| 2728 | // - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any | ||
| 2729 | // further, the feedback that had been gating each pass was gone, and the | ||
| 2730 | // decay began compounding once a second against the total elapsed | ||
| 2731 | // silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in | ||
| 2732 | // the next hundred seconds, four hours in, for no reason a reader of | ||
| 2733 | // either the code or the announced number could see. | ||
| 2734 | // | ||
| 2735 | // Computing the value instead of accumulating it removes the failure mode | ||
| 2736 | // rather than tuning it: with no stored decay state there is nothing for a | ||
| 2737 | // tick rate, a missed pass or a duplicated pass to perturb, and the answer | ||
| 2738 | // depends only on how long the miner has actually been quiet. It also keeps | ||
| 2739 | // the fade out of the estimator -- the EWMA on the share path still blends | ||
| 2740 | // against the value it last computed, not against a faded copy of it, so a | ||
| 2741 | // miner that resumes recovers on exactly the curve it would have without any | ||
| 2742 | // of this. | ||
| 2743 | // | ||
| 2744 | // `now` is a parameter rather than a GetTime() call so that one refresh fades | ||
| 2745 | // every miner to the same instant, and so the policy is directly testable. | ||
| 2746 | static double MinerReportedHashRate(const Miner* m, int64 now) | ||
| 2747 | { | ||
| 2748 | // Nothing sampled yet: a fresh connection, or one that has found a single | ||
| 2749 | // share and so has no interval to have been timed over. Nothing to fade, | ||
| 2750 | // and nothing to divide by below. | ||
| 2751 | if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate; | ||
| 2752 | |||
| 2753 | double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate; | ||
| 2754 | // Written so that a NaN would land on the floor rather than sail through: | ||
| 2755 | // the clamp is what guarantees `expected` is finite and strictly positive | ||
| 2756 | // below, whatever the division produced. | ||
| 2757 | if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL; | ||
| 2758 | if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL; | ||
| 2759 | |||
| 2760 | double silent = (double)(now - m->lastShareTime); | ||
| 2761 | double grace = HASHRATE_IDLE_MISSES * expected; | ||
| 2762 | // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has | ||
| 2763 | // stepped backwards gives a negative `silent`, takes this branch, and | ||
| 2764 | // reports the sample unchanged. Nothing here can scale an estimate up. | ||
| 2765 | if (!(silent > grace)) return m->hashRate; | ||
| 2766 | |||
| 2767 | double halflives = (silent - grace) / expected; | ||
| 2768 | if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0; | ||
| 2769 | return m->hashRate * pow(0.5, halflives); | ||
| 2770 | } | ||
| 2771 | |||
| 2506 | static std::vector<Miner*> gMiners; | 2772 | static std::vector<Miner*> gMiners; |
| 2507 | 2773 | ||
| 2508 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2774 | struct FoundBlock { int height; int64 ts; std::string addr; }; |
| @@ -2553,6 +2819,18 @@ static void LocalAcceptFn(void*) | |||
| 2553 | break; | 2819 | break; |
| 2554 | continue; | 2820 | continue; |
| 2555 | } | 2821 | } |
| 2822 | // #6: let the OS probe a peer that has gone silent, so a half-open | ||
| 2823 | // session eventually errors out of recv and lands in the event | ||
| 2824 | // loop's existing reap path instead of sitting in gMiners forever. | ||
| 2825 | // This is a backstop, not the mechanism: the default cadence before | ||
| 2826 | // the first probe is hours on most stacks, so the idle timeout in | ||
| 2827 | // PoolEventLoop fires long before keepalive does. Miners arriving | ||
| 2828 | // via a relay ride a loopback socket pair whose remote transport is | ||
| 2829 | // the rendezvous socket owned by the tunnel pump threads; keepalive | ||
| 2830 | // cannot reach across that, which is part of why the idle timeout | ||
| 2831 | // exists. | ||
| 2832 | int keepalive = 1; | ||
| 2833 | setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive)); | ||
| 2556 | LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); | 2834 | LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); |
| 2557 | PushReady((btf_socket_t)c); | 2835 | PushReady((btf_socket_t)c); |
| 2558 | } | 2836 | } |
| @@ -2596,6 +2874,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it | |||
| 2596 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not | 2874 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not |
| 2597 | // authorized after this long is | 2875 | // authorized after this long is |
| 2598 | // idle, not merely starting up | 2876 | // idle, not merely starting up |
| 2877 | // #6: a Miner used to leave gMiners only when RecvLine reported a clean | ||
| 2878 | // disconnect. A half-open session (miner host died, NAT state expired, | ||
| 2879 | // cable pulled) sends no FIN and reports no error, so it sat in the table | ||
| 2880 | // forever -- authorize outpaced disconnect 2:1 on the live pool, and the | ||
| 2881 | // table grew toward MAX_POOL_MINERS, past which real miners are refused. | ||
| 2882 | // lastSeen is stamped on every complete line received; a miner that has | ||
| 2883 | // said nothing for this long is gone in every way that matters. Ten | ||
| 2884 | // minutes is far beyond the vardiff target (a share every 15s) and the | ||
| 2885 | // stall watchdog (45s), so no miner that is actually hashing comes close. | ||
| 2886 | static const int64 POOL_IDLE_TIMEOUT_SECS = 600; | ||
| 2599 | 2887 | ||
| 2600 | // Non-blocking recv: pull whatever bytes are available now into buf, then | 2888 | // Non-blocking recv: pull whatever bytes are available now into buf, then |
| 2601 | // return one complete line if one is ready. Three outcomes: | 2889 | // return one complete line if one is ready. Three outcomes: |
| @@ -2680,6 +2968,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b, | |||
| 2680 | static void SendJob(Miner* m, bool clean) | 2968 | static void SendJob(Miner* m, bool clean) |
| 2681 | { | 2969 | { |
| 2682 | if (!gHaveJob) return; | 2970 | if (!gHaveJob) return; |
| 2971 | // Every path that hands a miner a new job funnels through here, so this | ||
| 2972 | // is the one place the replay guard needs to follow the job id: nonces | ||
| 2973 | // credited under the previous job mean nothing under the new one. Keying | ||
| 2974 | // the clear to the id (rather than clearing unconditionally) keeps a | ||
| 2975 | // same-job re-notify -- the vardiff retarget and stall watchdog both | ||
| 2976 | // re-send the CURRENT job -- from reopening the duplicate window it | ||
| 2977 | // exists to close. | ||
| 2978 | if (m->seenNoncesJob != gCurrentJob.jobId) { | ||
| 2979 | m->seenNonces.clear(); | ||
| 2980 | m->seenNoncesJob = gCurrentJob.jobId; | ||
| 2981 | } | ||
| 2683 | CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); | 2982 | CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); |
| 2684 | // Use this miner's own shareTarget (set by vardiff) in the notify params. | 2983 | // Use this miner's own shareTarget (set by vardiff) in the notify params. |
| 2685 | // The participant miner reads p[3] as its share target, and HandleLine | 2984 | // The participant miner reads p[3] as its share target, and HandleLine |
| @@ -2938,6 +3237,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 2938 | } | 3237 | } |
| 2939 | unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); | 3238 | unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); |
| 2940 | 3239 | ||
| 3240 | // Reject a replayed (job, nonce) before it costs a rebuild and a | ||
| 3241 | // RandomX hash: nothing about the work below distinguishes a second | ||
| 3242 | // submission of a valid share from the first, so without this check | ||
| 3243 | // it passes every test and is credited again -- and roundShareCount | ||
| 3244 | // is what the payout split divides by, so each replay multiplies the | ||
| 3245 | // address's cut of the round. The set is per-connection and cleared | ||
| 3246 | // by SendJob on every job change, so only a same-job repeat is | ||
| 3247 | // refused. | ||
| 3248 | if (m->seenNonces.count(nNonce)) { | ||
| 3249 | LogPrint("worker", "[worker->pool] duplicate share from %s" | ||
| 3250 | " nonce=%u job=%s -- already credited, not counting again\n", | ||
| 3251 | m->address.c_str(), nNonce, submitJobId.c_str()); | ||
| 3252 | reply(false, "Duplicate share"); | ||
| 3253 | return true; | ||
| 3254 | } | ||
| 3255 | |||
| 2941 | // Rebuild this miner's exact block (unique merkle via extranonce2) | 3256 | // Rebuild this miner's exact block (unique merkle via extranonce2) |
| 2942 | CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); | 3257 | CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); |
| 2943 | b.nNonce = nNonce; | 3258 | b.nNonce = nNonce; |
| @@ -2965,6 +3280,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 2965 | m->roundShares++; | 3280 | m->roundShares++; |
| 2966 | m->totalShares++; | 3281 | m->totalShares++; |
| 2967 | m->vardiffShares++; | 3282 | m->vardiffShares++; |
| 3283 | m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job | ||
| 2968 | int64 shareTime = GetTime(); | 3284 | int64 shareTime = GetTime(); |
| 2969 | m->lastSeen = shareTime; | 3285 | m->lastSeen = shareTime; |
| 2970 | roundShareCount[m->address]++; | 3286 | roundShareCount[m->address]++; |
| @@ -3261,7 +3577,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment, | |||
| 3261 | int64 amount, | 3577 | int64 amount, |
| 3262 | int nonce, | 3578 | int nonce, |
| 3263 | const std::string& structuredId, | 3579 | const std::string& structuredId, |
| 3264 | bool fConfirmed = true) | 3580 | bool fConfirmed = true, |
| 3581 | const std::string& structuredBlock = "") | ||
| 3265 | { | 3582 | { |
| 3266 | CWalletTx wtx; | 3583 | CWalletTx wtx; |
| 3267 | wtx.vin.resize(1); | 3584 | wtx.vin.resize(1); |
| @@ -3276,6 +3593,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment, | |||
| 3276 | wtx.mapValue["pool_payout_id"] = structuredId; | 3593 | wtx.mapValue["pool_payout_id"] = structuredId; |
| 3277 | wtx.mapValue["pool_payout_address"] = address; | 3594 | wtx.mapValue["pool_payout_address"] = address; |
| 3278 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); | 3595 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); |
| 3596 | if (!structuredBlock.empty()) | ||
| 3597 | wtx.mapValue["pool_payout_block"] = structuredBlock; | ||
| 3279 | } | 3598 | } |
| 3280 | uint256 hash = wtx.GetHash(); | 3599 | uint256 hash = wtx.GetHash(); |
| 3281 | if (fConfirmed) { | 3600 | if (fConfirmed) { |
| @@ -3825,34 +4144,37 @@ int RunPayoutLedgerSelfTest() | |||
| 3825 | const int height = 8080; | 4144 | const int height = 8080; |
| 3826 | const int64 amount = 7 * COIN; | 4145 | const int64 amount = 7 * COIN; |
| 3827 | const std::string id = PayoutIdForBlock(height, 0); | 4146 | const std::string id = PayoutIdForBlock(height, 0); |
| 3828 | const std::string comment = PayoutComment(height); | 4147 | const std::string comment = LegacyPayoutComment(height); |
| 3829 | std::set<std::string> none; | 4148 | std::set<std::string> none; |
| 3830 | std::string found; | 4149 | std::string found; |
| 4150 | // No round proof exists for these rounds, so legacy evidence is | ||
| 4151 | // admissible -- computed exactly as the payout thread computes it. | ||
| 4152 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 3831 | 4153 | ||
| 3832 | check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) | 4154 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 3833 | == PAYOUT_PROBE_NONE, | 4155 | == PAYOUT_PROBE_NONE, |
| 3834 | "an empty wallet is not evidence that anything was paid"); | 4156 | "an empty wallet is not evidence that anything was paid"); |
| 3835 | 4157 | ||
| 3836 | const std::string legacyTxid = | 4158 | const std::string legacyTxid = |
| 3837 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); | 4159 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); |
| 3838 | check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) | 4160 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 3839 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, | 4161 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, |
| 3840 | "a payment carrying only a pre-upgrade comment is recognised as" | 4162 | "a payment carrying only a pre-upgrade comment is recognised as" |
| 3841 | " already paid"); | 4163 | " already paid"); |
| 3842 | 4164 | ||
| 3843 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4165 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3844 | == PAYOUT_PROBE_NONE, | 4166 | == PAYOUT_PROBE_NONE, |
| 3845 | "the legacy match requires the output to pay THIS recipient"); | 4167 | "the legacy match requires the output to pay THIS recipient"); |
| 3846 | check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found) | 4168 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, fAllowLegacy, none, found) |
| 3847 | == PAYOUT_PROBE_NONE, | 4169 | == PAYOUT_PROBE_NONE, |
| 3848 | "the legacy match requires the output to pay exactly this amount"); | 4170 | "the legacy match requires the output to pay exactly this amount"); |
| 3849 | check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found) | 4171 | check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, fAllowLegacy, none, found) |
| 3850 | == PAYOUT_PROBE_NONE, | 4172 | == PAYOUT_PROBE_NONE, |
| 3851 | "the legacy match requires the comment for this block height"); | 4173 | "the legacy match requires the comment for this block height"); |
| 3852 | 4174 | ||
| 3853 | std::set<std::string> spent; | 4175 | std::set<std::string> spent; |
| 3854 | spent.insert(legacyTxid); | 4176 | spent.insert(legacyTxid); |
| 3855 | check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found) | 4177 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, spent, found) |
| 3856 | == PAYOUT_PROBE_NONE, | 4178 | == PAYOUT_PROBE_NONE, |
| 3857 | "one legacy transaction cannot settle a second debt"); | 4179 | "one legacy transaction cannot settle a second debt"); |
| 3858 | 4180 | ||
| @@ -3861,7 +4183,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3861 | // exactly what the structured metadata exists to prevent. | 4183 | // exactly what the structured metadata exists to prevent. |
| 3862 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; | 4184 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; |
| 3863 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); | 4185 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); |
| 3864 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4186 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3865 | == PAYOUT_PROBE_NONE, | 4187 | == PAYOUT_PROBE_NONE, |
| 3866 | "another round's structured payment is not legacy evidence for" | 4188 | "another round's structured payment is not legacy evidence for" |
| 3867 | " this one"); | 4189 | " this one"); |
| @@ -3869,7 +4191,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3869 | // The structured path still wins where it applies. | 4191 | // The structured path still wins where it applies. |
| 3870 | const std::string structuredTxid = | 4192 | const std::string structuredTxid = |
| 3871 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); | 4193 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); |
| 3872 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4194 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3873 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, | 4195 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, |
| 3874 | "structured payout metadata is matched in preference to a comment"); | 4196 | "structured payout metadata is matched in preference to a comment"); |
| 3875 | 4197 | ||
| @@ -3883,7 +4205,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3883 | PayoutTestClearBlockIndex(); | 4205 | PayoutTestClearBlockIndex(); |
| 3884 | const std::string addrC = PayoutTestAddress(0x33); | 4206 | const std::string addrC = PayoutTestAddress(0x33); |
| 3885 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); | 4207 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); |
| 3886 | check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) | 4208 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 3887 | == PAYOUT_PROBE_NONE, | 4209 | == PAYOUT_PROBE_NONE, |
| 3888 | "a pre-upgrade payout that never confirmed is not read as" | 4210 | "a pre-upgrade payout that never confirmed is not read as" |
| 3889 | " payment"); | 4211 | " payment"); |
| @@ -3893,7 +4215,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3893 | mapWallet.clear(); | 4215 | mapWallet.clear(); |
| 3894 | const std::string confirmedTxid = | 4216 | const std::string confirmedTxid = |
| 3895 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); | 4217 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); |
| 3896 | check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) | 4218 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 3897 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, | 4219 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, |
| 3898 | "the same payout, confirmed, is"); | 4220 | "the same payout, confirmed, is"); |
| 3899 | 4221 | ||
| @@ -3904,7 +4226,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3904 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); | 4226 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); |
| 3905 | std::set<std::string> spentFirst; | 4227 | std::set<std::string> spentFirst; |
| 3906 | spentFirst.insert(confirmedTxid); | 4228 | spentFirst.insert(confirmedTxid); |
| 3907 | check(FindRecordedPoolPayout(id, height, addrC, amount, spentFirst, found) | 4229 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, spentFirst, found) |
| 3908 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, | 4230 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, |
| 3909 | "an unconfirmed transaction does not hide a confirmed one behind" | 4231 | "an unconfirmed transaction does not hide a confirmed one behind" |
| 3910 | " it"); | 4232 | " it"); |
| @@ -4419,6 +4741,248 @@ int RunPayoutLedgerSelfTest() | |||
| 4419 | } | 4741 | } |
| 4420 | #endif | 4742 | #endif |
| 4421 | 4743 | ||
| 4744 | // -- 14. The payout comment names the funding block ---------------------- | ||
| 4745 | // | ||
| 4746 | // Same height, two hashes: the shape a reorg leaves when the pool wins a | ||
| 4747 | // height, loses the block, and wins the height again. The legacy needle is | ||
| 4748 | // frozen at the pre-upgrade format, so pre-upgrade payments keep matching; | ||
| 4749 | // a new-format comment never matches it, so a new send can never be read | ||
| 4750 | // as a pre-upgrade payment for a twin round. | ||
| 4751 | { | ||
| 4752 | PayoutTestResetState(); | ||
| 4753 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4754 | mapWallet.clear(); | ||
| 4755 | PayoutTestClearBlockIndex(); | ||
| 4756 | |||
| 4757 | const int height = 9090; | ||
| 4758 | const int64 amount = 3 * COIN; | ||
| 4759 | uint256 hashA = 0, hashB = 0; | ||
| 4760 | hashA.SetHex("00000000000000000000000000000000000000000000000000000000000000aa"); | ||
| 4761 | hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000bb"); | ||
| 4762 | |||
| 4763 | check(LegacyPayoutComment(height) == | ||
| 4764 | strprintf("Pool payout height %d", height), | ||
| 4765 | "the legacy needle is frozen at the format pre-upgrade builds" | ||
| 4766 | " wrote"); | ||
| 4767 | check(PayoutComment(height, 0) == LegacyPayoutComment(height), | ||
| 4768 | "a grandfathered zero-hash record still writes the legacy form"); | ||
| 4769 | check(PayoutComment(height, hashA) == | ||
| 4770 | strprintf("Pool payout height %d block %s", height, | ||
| 4771 | hashA.ToString().c_str()), | ||
| 4772 | "a hash-keyed record's comment names its funding block"); | ||
| 4773 | check(PayoutComment(height, hashA) != PayoutComment(height, hashB), | ||
| 4774 | "twin rounds at one height carry distinguishable comments"); | ||
| 4775 | |||
| 4776 | // A transaction wearing the NEW comment format and -- hypothetically | ||
| 4777 | // -- no structured metadata must not be matched by the legacy needle: | ||
| 4778 | // the fallback exists for pre-upgrade transactions, and the new format | ||
| 4779 | // proves this is not one. | ||
| 4780 | const std::string idA = PayoutIdForBlock(height, hashA); | ||
| 4781 | const std::string idB = PayoutIdForBlock(height, hashB); | ||
| 4782 | std::set<std::string> none; | ||
| 4783 | std::string found; | ||
| 4784 | // Neither twin has a round proof here (section 15 covers the gate), | ||
| 4785 | // and their ids collide, so one answer serves both. | ||
| 4786 | const bool fAllowLegacy = !PoolRoundHasProof(idA); | ||
| 4787 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, ""); | ||
| 4788 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4789 | == PAYOUT_PROBE_NONE, | ||
| 4790 | "a new-format comment is never legacy evidence for a twin round"); | ||
| 4791 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4792 | == PAYOUT_PROBE_NONE, | ||
| 4793 | "nor a match channel for its own round -- structured metadata is"); | ||
| 4794 | |||
| 4795 | // Round A's own payment, sent by this build, is found by its | ||
| 4796 | // structured id. | ||
| 4797 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4798 | mapWallet.clear(); | ||
| 4799 | PayoutTestClearBlockIndex(); | ||
| 4800 | const std::string paidA = | ||
| 4801 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | ||
| 4802 | 2, idA); | ||
| 4803 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4804 | == PAYOUT_PROBE_STRUCTURED && found == paidA, | ||
| 4805 | "the winning twin's own payment is found by its structured id"); | ||
| 4806 | |||
| 4807 | // The id is derived from the FIRST sixteen hex digits of the hash -- | ||
| 4808 | // the end proof-of-work fills with zeros -- so twin hashes that agree | ||
| 4809 | // there collide in id, and a payment carrying no pool_payout_block | ||
| 4810 | // answers for either twin. That is the accepted trade for payments | ||
| 4811 | // sent before the key existed, pinned here so it stays a decision. | ||
| 4812 | check(idA == idB, | ||
| 4813 | "twin hashes differing only past the sixteenth hex digit collide" | ||
| 4814 | " in id -- the reason the full hash must decide"); | ||
| 4815 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4816 | == PAYOUT_PROBE_STRUCTURED, | ||
| 4817 | "a payment without the block key answers for a colliding twin --" | ||
| 4818 | " the accepted residual for pre-key transactions"); | ||
| 4819 | |||
| 4820 | // With the full hash recorded -- as every send now records it -- the | ||
| 4821 | // twins are told apart and the re-won height is paid. | ||
| 4822 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4823 | mapWallet.clear(); | ||
| 4824 | PayoutTestClearBlockIndex(); | ||
| 4825 | const std::string keyedA = | ||
| 4826 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | ||
| 4827 | 3, idA, true, hashA.ToString()); | ||
| 4828 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4829 | == PAYOUT_PROBE_STRUCTURED && found == keyedA, | ||
| 4830 | "a payment carrying the full block hash settles its own round"); | ||
| 4831 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4832 | == PAYOUT_PROBE_NONE, | ||
| 4833 | "and is no evidence for the twin, so the re-won height is paid"); | ||
| 4834 | |||
| 4835 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4836 | mapWallet.clear(); | ||
| 4837 | PayoutTestClearBlockIndex(); | ||
| 4838 | } | ||
| 4839 | |||
| 4840 | // -- 15. A proof-bearing round rejects comment-only evidence ------------- | ||
| 4841 | // | ||
| 4842 | // The surviving half of the same-height collision: a pre-upgrade | ||
| 4843 | // transaction carries only "Pool payout height H", so if H is won again | ||
| 4844 | // after a reorg, it matches the new round's (height, address, amount) | ||
| 4845 | // exactly and would settle a debt it never paid. The new round has a | ||
| 4846 | // round proof -- written at creation, and only by builds whose payments | ||
| 4847 | // all name their round -- so for it, comment-only evidence is | ||
| 4848 | // inadmissible and the miner is paid. A round without a proof keeps | ||
| 4849 | // today's bias: the evidence is admissible and doubt resolves to | ||
| 4850 | // "already paid". | ||
| 4851 | { | ||
| 4852 | PayoutTestResetState(); | ||
| 4853 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4854 | mapWallet.clear(); | ||
| 4855 | PayoutTestClearBlockIndex(); | ||
| 4856 | const std::string roundsPath = dir + "/pool_rounds_gate_selftest.json"; | ||
| 4857 | const std::string savedRoundsFile = strPoolRoundsFile; | ||
| 4858 | strPoolRoundsFile = roundsPath; | ||
| 4859 | remove(roundsPath.c_str()); | ||
| 4860 | |||
| 4861 | const int height = 7171; | ||
| 4862 | const int64 amount = 4 * COIN; | ||
| 4863 | uint256 hashB = 0; | ||
| 4864 | hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000cc"); | ||
| 4865 | const std::string idB = PayoutIdForBlock(height, hashB); | ||
| 4866 | std::set<std::string> none; | ||
| 4867 | std::string found; | ||
| 4868 | |||
| 4869 | // The colliding pre-upgrade payment: confirmed, comment-only, same | ||
| 4870 | // height, same address, same amount -- the strongest wrong evidence | ||
| 4871 | // there is. | ||
| 4872 | PayoutTestAddWalletTx(LegacyPayoutComment(height), addrA, amount, 1, ""); | ||
| 4873 | |||
| 4874 | check(!PoolRoundHasProof(idB), | ||
| 4875 | "a round with no recorded proof does not claim one"); | ||
| 4876 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4877 | !PoolRoundHasProof(idB), none, found) | ||
| 4878 | == PAYOUT_PROBE_LEGACY, | ||
| 4879 | "without a proof the doubt is genuine and still resolves to" | ||
| 4880 | " already-paid"); | ||
| 4881 | |||
| 4882 | PoolRoundProof round; | ||
| 4883 | round.id = idB; | ||
| 4884 | round.blockHeight = height; | ||
| 4885 | round.matureAtHeight = height + COINBASE_SPENDABLE_DEPTH; | ||
| 4886 | round.payouts[addrA] = amount; | ||
| 4887 | RecordPoolRound(round); | ||
| 4888 | check(PoolRoundHasProof(idB), | ||
| 4889 | "recording the round proof marks the round as this lineage's"); | ||
| 4890 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4891 | !PoolRoundHasProof(idB), none, found) | ||
| 4892 | == PAYOUT_PROBE_NONE, | ||
| 4893 | "with a proof, a same-height comment-only transaction is not" | ||
| 4894 | " evidence, and the re-won height is paid"); | ||
| 4895 | |||
| 4896 | // Structured evidence still is: the round's own payment, recovered. | ||
| 4897 | const std::string paidB = | ||
| 4898 | PayoutTestAddWalletTx(PayoutComment(height, hashB), addrA, amount, | ||
| 4899 | 2, idB, true, hashB.ToString()); | ||
| 4900 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4901 | !PoolRoundHasProof(idB), none, found) | ||
| 4902 | == PAYOUT_PROBE_STRUCTURED && found == paidB, | ||
| 4903 | "the gate does not blind the probe to the round's own structured" | ||
| 4904 | " payment"); | ||
| 4905 | |||
| 4906 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4907 | mapWallet.clear(); | ||
| 4908 | PayoutTestClearBlockIndex(); | ||
| 4909 | remove(roundsPath.c_str()); | ||
| 4910 | remove((roundsPath + ".tmp").c_str()); | ||
| 4911 | strPoolRoundsFile = savedRoundsFile; | ||
| 4912 | } | ||
| 4913 | |||
| 4914 | // -- 16. A malformed block hash is corruption, not a hash ---------------- | ||
| 4915 | // | ||
| 4916 | // SetHex() parses whatever hex prefix it finds and stops, so a corrupt | ||
| 4917 | // blockHash did not fail to load -- it loaded as a DIFFERENT hash. | ||
| 4918 | // "deadbeef..." became 0xdeadbeef, a hash no block ever had, which the | ||
| 4919 | // orphan check reads as an orphaned block and the next flush VOIDS the | ||
| 4920 | // debt; a value with no hex prefix became 0, which is grandfathered past | ||
| 4921 | // the orphan check instead. Either way a corrupt file silently changed | ||
| 4922 | // what the pool owed. It is a load failure now, handled like every other | ||
| 4923 | // parse error: quarantine, and never rewrite. | ||
| 4924 | { | ||
| 4925 | PayoutTestResetState(); | ||
| 4926 | remove(ledgerPath.c_str()); | ||
| 4927 | remove(payingPath.c_str()); | ||
| 4928 | |||
| 4929 | const std::string badLedger = strprintf( | ||
| 4930 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4931 | "\"blockHash\":\"deadbeef oops, not a hash\"," | ||
| 4932 | "\"blockHeight\":10,\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4933 | check(PayoutTestWriteFile(ledgerPath, badLedger), | ||
| 4934 | "wrote a ledger whose blockHash is not a 64-digit hex string"); | ||
| 4935 | LoadPendingPayouts(); | ||
| 4936 | { | ||
| 4937 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4938 | check(gPendingPayouts.empty(), | ||
| 4939 | "no entry is loaded with a guessed hash"); | ||
| 4940 | } | ||
| 4941 | std::string gone; | ||
| 4942 | check(!PayoutTestReadFile(ledgerPath, gone), | ||
| 4943 | "the ledger with the corrupt hash is quarantined"); | ||
| 4944 | |||
| 4945 | const std::string badPaying = strprintf( | ||
| 4946 | "[{\"address\":\"%s\",\"amount\":100000000," | ||
| 4947 | "\"blockHash\":\"00000000cafe\",\"blockHeight\":10," | ||
| 4948 | "\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4949 | check(PayoutTestWriteFile(payingPath, badPaying), | ||
| 4950 | "wrote an in-flight list whose blockHash is truncated"); | ||
| 4951 | LoadPaying(); | ||
| 4952 | { | ||
| 4953 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); | ||
| 4954 | check(gPaying.empty(), | ||
| 4955 | "no in-flight record is loaded with a guessed hash"); | ||
| 4956 | } | ||
| 4957 | check(!PayoutTestReadFile(payingPath, gone), | ||
| 4958 | "the in-flight list with the corrupt hash is quarantined"); | ||
| 4959 | |||
| 4960 | // The two spellings every real file uses still load: the 64-digit | ||
| 4961 | // hash of a keyed round, and the 64 zeros this build writes for a | ||
| 4962 | // grandfathered entry. | ||
| 4963 | PayoutTestResetState(); | ||
| 4964 | remove(ledgerPath.c_str()); | ||
| 4965 | const std::string goodLedger = strprintf( | ||
| 4966 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4967 | "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\"," | ||
| 4968 | "\"blockHeight\":10,\"matureAtHeight\":10}," | ||
| 4969 | "{\"amounts\":{\"%s\":200000000}," | ||
| 4970 | "\"blockHash\":\"%s\"," | ||
| 4971 | "\"blockHeight\":20,\"matureAtHeight\":20}]", | ||
| 4972 | addrA.c_str(), addrB.c_str(), uint256(0).ToString().c_str()); | ||
| 4973 | check(PayoutTestWriteFile(ledgerPath, goodLedger), | ||
| 4974 | "wrote a ledger in both real hash spellings"); | ||
| 4975 | LoadPendingPayouts(); | ||
| 4976 | { | ||
| 4977 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4978 | check(gPendingPayouts.size() == 2 && | ||
| 4979 | gPendingPayouts[0].blockHash != 0 && | ||
| 4980 | gPendingPayouts[1].blockHash == 0, | ||
| 4981 | "a real hash and a serialised zero hash both still load"); | ||
| 4982 | } | ||
| 4983 | remove(ledgerPath.c_str()); | ||
| 4984 | } | ||
| 4985 | |||
| 4422 | // -- clean up ------------------------------------------------------------ | 4986 | // -- clean up ------------------------------------------------------------ |
| 4423 | PayoutTestResetState(); | 4987 | PayoutTestResetState(); |
| 4424 | CRITICAL_BLOCK(cs_mapWallet) | 4988 | CRITICAL_BLOCK(cs_mapWallet) |
| @@ -4579,6 +5143,82 @@ int RunPoolStratumSelfTest() | |||
| 4579 | strPoolRoundsFile = savedRoundsFile; | 5143 | strPoolRoundsFile = savedRoundsFile; |
| 4580 | gPoolRounds = savedRounds; | 5144 | gPoolRounds = savedRounds; |
| 4581 | 5145 | ||
| 5146 | // -- hashrate decay (#7) -------------------------------------------------- | ||
| 5147 | // The first version of this decayed m->hashRate in place from the event | ||
| 5148 | // loop's sweep, which made the reported figure a function of how the loop | ||
| 5149 | // happened to run rather than of how long the miner had been quiet. These | ||
| 5150 | // pin the properties that fixes: sampling changes nothing, and the answer | ||
| 5151 | // depends only on elapsed silence. | ||
| 5152 | { | ||
| 5153 | // A miner exactly on the vardiff target: 4096 hashes per share at | ||
| 5154 | // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected | ||
| 5155 | // interval is 15s and its grace is 60s. | ||
| 5156 | const double base = 4096.0 / 15.0; | ||
| 5157 | const int64 t0 = 1766000000; // any plausible non-zero wall clock | ||
| 5158 | Miner decayed(INVALID_SOCKET); | ||
| 5159 | decayed.difficulty = 1.0; | ||
| 5160 | decayed.hashRate = base; | ||
| 5161 | decayed.lastShareTime = t0; | ||
| 5162 | |||
| 5163 | check(MinerReportedHashRate(&decayed, t0 + 60) == base, | ||
| 5164 | "hashrate decay: a miner inside its grace period reports its full estimate"); | ||
| 5165 | |||
| 5166 | // Sampling must not consume the estimate. Twenty passes is one second | ||
| 5167 | // of the event loop's old sweep; a thousand is nearly a minute of it. | ||
| 5168 | double once = MinerReportedHashRate(&decayed, t0 + 61); | ||
| 5169 | bool idempotent = (decayed.hashRate == base); | ||
| 5170 | for (int i = 0; i < 1000 && idempotent; i++) | ||
| 5171 | idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) && | ||
| 5172 | (decayed.hashRate == base); | ||
| 5173 | check(idempotent, | ||
| 5174 | "hashrate decay: repeated sampling returns one value and leaves the estimate alone"); | ||
| 5175 | |||
| 5176 | // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not | ||
| 5177 | // the estimate gone. A miner at the vardiff target overshoots its grace | ||
| 5178 | // roughly 1.8% of the time (e^-4) purely by share-timing variance, and | ||
| 5179 | // must not be zeroed for it. | ||
| 5180 | check(once > 0.94 * base && once < 0.96 * base, | ||
| 5181 | "hashrate decay: one second past grace costs a few percent, not the estimate"); | ||
| 5182 | double three = MinerReportedHashRate(&decayed, t0 + 63); | ||
| 5183 | check(three > 0.86 * base && three < 0.88 * base, | ||
| 5184 | "hashrate decay: three seconds past grace costs ~13%"); | ||
| 5185 | |||
| 5186 | // One half-life past the grace is exactly half. | ||
| 5187 | double half = MinerReportedHashRate(&decayed, t0 + 75); | ||
| 5188 | check(half > 0.499 * base && half < 0.501 * base, | ||
| 5189 | "hashrate decay: one expected interval past grace halves the estimate"); | ||
| 5190 | |||
| 5191 | // A miner that has genuinely stopped ends up reported as nothing -- | ||
| 5192 | // the whole point of #7. The old in-place version could not get here: | ||
| 5193 | // its decay fed back into its own grace and settled into a 1/t crawl. | ||
| 5194 | check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0, | ||
| 5195 | "hashrate decay: a miner silent for an hour is reported as zero"); | ||
| 5196 | |||
| 5197 | // A clock that steps backwards must never inflate what we announce. | ||
| 5198 | check(MinerReportedHashRate(&decayed, t0 - 3600) == base, | ||
| 5199 | "hashrate decay: a backwards clock reports the estimate unchanged, never higher"); | ||
| 5200 | |||
| 5201 | // Fresh connection, and a connection with one share and so no interval | ||
| 5202 | // to have timed: both must report nothing rather than divide by zero. | ||
| 5203 | Miner fresh(INVALID_SOCKET); | ||
| 5204 | check(MinerReportedHashRate(&fresh, t0) == 0.0, | ||
| 5205 | "hashrate decay: a miner that never submitted a share reports zero"); | ||
| 5206 | Miner single(INVALID_SOCKET); | ||
| 5207 | single.lastShareTime = t0; | ||
| 5208 | check(MinerReportedHashRate(&single, t0 + 86400) == 0.0, | ||
| 5209 | "hashrate decay: a miner with no rate sample yet reports zero"); | ||
| 5210 | |||
| 5211 | // Nothing in the swept range may produce a NaN, an infinity, a negative | ||
| 5212 | // rate, or a rate above the estimate it started from. | ||
| 5213 | bool finite = true; | ||
| 5214 | for (int64 s = -100; s < 200000 && finite; s += 37) { | ||
| 5215 | double v = MinerReportedHashRate(&decayed, t0 + s); | ||
| 5216 | finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base; | ||
| 5217 | } | ||
| 5218 | check(finite, | ||
| 5219 | "hashrate decay: stays finite, non-negative and non-amplifying across a long silence"); | ||
| 5220 | } | ||
| 5221 | |||
| 4582 | gCurrentJob = savedJob; | 5222 | gCurrentJob = savedJob; |
| 4583 | gHaveJob = savedHaveJob; | 5223 | gHaveJob = savedHaveJob; |
| 4584 | 5224 | ||
| @@ -4606,6 +5246,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) | |||
| 4606 | if (m->authorised) gStats.authorizedMiners++; | 5246 | if (m->authorised) gStats.authorizedMiners++; |
| 4607 | 5247 | ||
| 4608 | gWorkerStats.clear(); | 5248 | gWorkerStats.clear(); |
| 5249 | // One timestamp for the whole sweep (#7): every miner is faded to the | ||
| 5250 | // same instant, so the per-worker rows and the total they are summed | ||
| 5251 | // into describe one moment rather than a smear across the loop. | ||
| 5252 | int64 nowStats = GetTime(); | ||
| 4609 | for (Miner* m : gMiners) { | 5253 | for (Miner* m : gMiners) { |
| 4610 | if (!m->authorised || m->lastSeen <= 0) continue; | 5254 | if (!m->authorised || m->lastSeen <= 0) continue; |
| 4611 | PoolWorkerStatView v; | 5255 | PoolWorkerStatView v; |
| @@ -4614,9 +5258,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) | |||
| 4614 | v.roundShares = m->roundShares; | 5258 | v.roundShares = m->roundShares; |
| 4615 | v.totalShares = m->totalShares; | 5259 | v.totalShares = m->totalShares; |
| 4616 | v.lastSeen = m->lastSeen; | 5260 | v.lastSeen = m->lastSeen; |
| 4617 | v.hashRate = m->hashRate; | 5261 | // Not m->hashRate: a miner that has stopped submitting shares must |
| 5262 | // not keep contributing its last sample to the announced total. | ||
| 5263 | v.hashRate = MinerReportedHashRate(m, nowStats); | ||
| 4618 | gWorkerStats.push_back(v); | 5264 | gWorkerStats.push_back(v); |
| 4619 | gStats.totalHashRate += m->hashRate; | 5265 | gStats.totalHashRate += v.hashRate; |
| 4620 | } | 5266 | } |
| 4621 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), | 5267 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), |
| 4622 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { | 5268 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { |
| @@ -4849,6 +5495,35 @@ static void PoolEventLoop() | |||
| 4849 | } | 5495 | } |
| 4850 | } | 5496 | } |
| 4851 | 5497 | ||
| 5498 | // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly -- | ||
| 5499 | // log, close, erase from gMiners via std::find, delete -- just with | ||
| 5500 | // a different reason. Shares the miner already earned stay in | ||
| 5501 | // roundShareCount, exactly as they do when a miner cleanly | ||
| 5502 | // disconnects mid-round. connectTime stands in for lastSeen on a | ||
| 5503 | // connection that never completed a line. Erasing inside the loop | ||
| 5504 | // above would invalidate the iteration, so collect then reap. | ||
| 5505 | { | ||
| 5506 | int64 now = GetTime(); | ||
| 5507 | std::vector<Miner*> idle; | ||
| 5508 | for (Miner* m : gMiners) { | ||
| 5509 | int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime; | ||
| 5510 | if (now - last > POOL_IDLE_TIMEOUT_SECS) | ||
| 5511 | idle.push_back(m); | ||
| 5512 | } | ||
| 5513 | for (Miner* m : idle) { | ||
| 5514 | LogPrint("worker", "[pool] dropping idle miner %s -- nothing" | ||
| 5515 | " received in over %llds (session shares: %llu," | ||
| 5516 | " round shares: %llu)\n", | ||
| 5517 | m->worker.empty() ? "(never authorized)" : m->worker.c_str(), | ||
| 5518 | (long long)POOL_IDLE_TIMEOUT_SECS, | ||
| 5519 | (unsigned long long)m->sessionShares, | ||
| 5520 | (unsigned long long)m->roundShares); | ||
| 5521 | sock_close(m->fd); | ||
| 5522 | gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m)); | ||
| 5523 | delete m; | ||
| 5524 | } | ||
| 5525 | } | ||
| 5526 | |||
| 4852 | // 4. Select over miner sockets | 5527 | // 4. Select over miner sockets |
| 4853 | if (!gMiners.empty()) { | 5528 | if (!gMiners.empty()) { |
| 4854 | fd_set fds; FD_ZERO(&fds); | 5529 | fd_set fds; FD_ZERO(&fds); |
| @@ -4871,6 +5546,11 @@ static void PoolEventLoop() | |||
| 4871 | // terminate the whole node -- not just the pool. Every | 5546 | // terminate the whole node -- not just the pool. Every |
| 4872 | // line arrives from an unauthenticated socket. | 5547 | // line arrives from an unauthenticated socket. |
| 4873 | while (RecvLine(m->fd, m->readBuf, line, disc)) { | 5548 | while (RecvLine(m->fd, m->readBuf, line, disc)) { |
| 5549 | // Any complete line is proof of life (#6): authorize | ||
| 5550 | // and valid shares already stamp lastSeen themselves, | ||
| 5551 | // but a miner that only ever subscribes (or sends | ||
| 5552 | // unrecognised methods) must count as alive too. | ||
| 5553 | m->lastSeen = GetTime(); | ||
| 4874 | try { | 5554 | try { |
| 4875 | HandleLine(m, line, roundShareCount, | 5555 | HandleLine(m, line, roundShareCount, |
| 4876 | roundShareTotal, blocksFoundThisSession); | 5556 | roundShareTotal, blocksFoundThisSession); |
| @@ -4947,13 +5627,19 @@ static void AcceptOneFn(void* arg) | |||
| 4947 | std::string host = relay.substr(0, colon); | 5627 | std::string host = relay.substr(0, colon); |
| 4948 | int port = atoi(relay.substr(colon + 1).c_str()); | 5628 | int port = atoi(relay.substr(colon + 1).c_str()); |
| 4949 | 5629 | ||
| 4950 | LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); | 5630 | LogPrint("pool", "[pool] accept thread started for relay %s%s\n", relay.c_str(), |
| 5631 | fBtfRelayPinned ? " (/rvrelay pin)" : ""); | ||
| 5632 | |||
| 5633 | // Registration health for this relay. A pin has no fallback, so a pin that | ||
| 5634 | // never registers means the pool is simply unreachable -- and that has to | ||
| 5635 | // be loud. The 2026-07-30 incident went unnoticed because this loop logged | ||
| 5636 | // failures at one flat line and success not at all, so "which relay is this | ||
| 5637 | // pool actually registered at" could not be answered from the log. | ||
| 5638 | int nConsecFail = 0; | ||
| 5639 | int64 nFirstFail = 0; | ||
| 5640 | bool fEverRegistered = false; | ||
| 4951 | 5641 | ||
| 4952 | while (!fShutdown && gPoolRunning) { | 5642 | while (!fShutdown && gPoolRunning) { |
| 4953 | // Advertise this relay as the pool's meeting node BEFORE registering, | ||
| 4954 | // so that miners which resolve our descriptor find a relay where the | ||
| 4955 | // pool (not the node's peer-accept) holds the registration. | ||
| 4956 | BtfSetActiveRelay(relay); | ||
| 4957 | // Returns as soon as the relay lists us -- registered, not yet paired. | 5643 | // Returns as soon as the relay lists us -- registered, not yet paired. |
| 4958 | btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); | 5644 | btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); |
| 4959 | 5645 | ||
| @@ -4962,12 +5648,36 @@ static void AcceptOneFn(void* arg) | |||
| 4962 | break; | 5648 | break; |
| 4963 | } | 5649 | } |
| 4964 | if (rv == btf::RV_INVALID) { | 5650 | if (rv == btf::RV_INVALID) { |
| 4965 | LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" | 5651 | nConsecFail++; |
| 4966 | " -- backing off 30s before retry\n", relay.c_str()); | 5652 | if (nFirstFail == 0) nFirstFail = GetTime(); |
| 5653 | LogPrint("pool", "[pool] relay %s: registration failed (attempt %d," | ||
| 5654 | " failing for %llds) -- backing off 30s before retry\n", | ||
| 5655 | relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail)); | ||
| 5656 | if (fBtfRelayPinned && (nConsecFail == 3 || nConsecFail % 10 == 0)) | ||
| 5657 | LogPrint("pool", "[pool] WARNING: pinned relay %s refused registration" | ||
| 5658 | " %d times running (%llds); /rvrelay leaves no fallback, so" | ||
| 5659 | " miners cannot reach this pool%s\n", | ||
| 5660 | relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail), | ||
| 5661 | fEverRegistered ? "" : " -- it has never registered once"); | ||
| 4967 | for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); | 5662 | for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); |
| 4968 | continue; | 5663 | continue; |
| 4969 | } | 5664 | } |
| 4970 | 5665 | ||
| 5666 | // Advertise THIS relay only now. Setting it before the call published a | ||
| 5667 | // meeting node we were not yet listed at, so a miner resolving the | ||
| 5668 | // descriptor inside that window burned its dial on a relay holding no | ||
| 5669 | // registration for us. net.cpp's peer-accept has always ordered it this | ||
| 5670 | // way round; the pool had not. | ||
| 5671 | BtfSetActiveRelay(relay); | ||
| 5672 | if (!fEverRegistered || nConsecFail > 0) | ||
| 5673 | LogPrint("pool", "[pool] registered at relay %s%s%s -- advertising it as" | ||
| 5674 | " the pool's meeting node\n", relay.c_str(), | ||
| 5675 | fBtfRelayPinned ? " (/rvrelay pin)" : "", | ||
| 5676 | nConsecFail > 0 ? ", recovered" : ""); | ||
| 5677 | fEverRegistered = true; | ||
| 5678 | nConsecFail = 0; | ||
| 5679 | nFirstFail = 0; | ||
| 5680 | |||
| 4971 | // Block for a miner to actually arrive. RvServiceRegister used to do | 5681 | // Block for a miner to actually arrive. RvServiceRegister used to do |
| 4972 | // this itself and returned with the relay's 0x01 pairing byte already | 5682 | // this itself and returned with the relay's 0x01 pairing byte already |
| 4973 | // consumed; it no longer does. Without waiting here, BtfServiceWrap | 5683 | // consumed; it no longer does. Without waiting here, BtfServiceWrap |
| @@ -5022,9 +5732,21 @@ static void AcceptLoopFn(void*) | |||
| 5022 | LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); | 5732 | LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); |
| 5023 | 5733 | ||
| 5024 | std::set<std::string> spawned; // relays we've already started a thread for | 5734 | std::set<std::string> spawned; // relays we've already started a thread for |
| 5735 | std::set<std::string> skipped; // discovered relays the pin excluded, logged once each | ||
| 5025 | 5736 | ||
| 5026 | while (!fShutdown && gPoolRunning) { | 5737 | while (!fShutdown && gPoolRunning) { |
| 5738 | // BtfAllRelays() already honours the pin: with /rvrelay set it returns | ||
| 5739 | // vBtfMeetingRelays alone, so no thread is ever spawned for a | ||
| 5740 | // discovered relay. Name the ones it drops -- a pool registering at a | ||
| 5741 | // stranger's relay is exactly the thing that should not be silent. | ||
| 5027 | vector<string> relays = BtfAllRelays(); | 5742 | vector<string> relays = BtfAllRelays(); |
| 5743 | if (fBtfRelayPinned) { | ||
| 5744 | for (const string& r : BtfDiscoveredRelays()) | ||
| 5745 | if (std::find(relays.begin(), relays.end(), r) == relays.end() && | ||
| 5746 | skipped.insert(r).second) | ||
| 5747 | LogPrint("pool", "[pool] not registering at discovered relay %s" | ||
| 5748 | " -- /rvrelay pins the relay list\n", r.c_str()); | ||
| 5749 | } | ||
| 5028 | if (relays.empty()) { | 5750 | if (relays.empty()) { |
| 5029 | LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); | 5751 | LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); |
| 5030 | } else { | 5752 | } else { |
| @@ -5040,8 +5762,9 @@ static void AcceptLoopFn(void*) | |||
| 5040 | relay.c_str()); | 5762 | relay.c_str()); |
| 5041 | } else { | 5763 | } else { |
| 5042 | spawned.insert(relay); | 5764 | spawned.insert(relay); |
| 5043 | LogPrint("pool", "[pool] accept thread spawned for relay %s" | 5765 | LogPrint("pool", "[pool] accept thread spawned for relay %s%s" |
| 5044 | " (%zu total)\n", relay.c_str(), spawned.size()); | 5766 | " (%zu total)\n", relay.c_str(), |
| 5767 | fBtfRelayPinned ? " (/rvrelay pin)" : "", spawned.size()); | ||
| 5045 | } | 5768 | } |
| 5046 | } | 5769 | } |
| 5047 | } | 5770 | } |
| @@ -5077,10 +5800,17 @@ void ThreadRPCServer(void*) | |||
| 5077 | // armed for its one wallet probe before it is allowed to send. | 5800 | // armed for its one wallet probe before it is allowed to send. |
| 5078 | ReconcilePayouts(); | 5801 | ReconcilePayouts(); |
| 5079 | 5802 | ||
| 5080 | _beginthread(AcceptLoopFn, 0, NULL); | 5803 | // _beginthread cannot throw (it reports -1), but an unchecked failure |
| 5081 | _beginthread(LocalAcceptFn, 0, NULL); | 5804 | // leaves the pool running deaf: nobody accepts, announces, or pays, and |
| 5082 | _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); | 5805 | // nothing in the log says why. |
| 5083 | _beginthread(PayoutThreadFn, 0, NULL); | 5806 | if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1) |
| 5807 | LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n"); | ||
| 5808 | if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1) | ||
| 5809 | LogPrint("pool", "[pool] local accept thread failed to spawn\n"); | ||
| 5810 | if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1) | ||
| 5811 | LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n"); | ||
| 5812 | if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1) | ||
| 5813 | LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n"); | ||
| 5084 | 5814 | ||
| 5085 | PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown | 5815 | PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown |
| 5086 | 5816 | ||
diff --git a/src/selftest.cpp b/src/selftest.cpp index 9f7106f..65f215b 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -10,9 +10,13 @@ | |||
| 10 | #include "bip32.h" | 10 | #include "bip32.h" |
| 11 | #include "proxy.h" | 11 | #include "proxy.h" |
| 12 | #include "selftest.h" | 12 | #include "selftest.h" |
| 13 | #ifndef _WIN32 | ||
| 14 | #include <sys/wait.h> // WIFEXITED/WEXITSTATUS for the child-process helper | ||
| 15 | #endif | ||
| 13 | #include "sockcount.h" | 16 | #include "sockcount.h" |
| 14 | #include "tor.h" | 17 | #include "tor.h" |
| 15 | #include "walletcmd.h" | 18 | #include "walletcmd.h" |
| 19 | #include "wallet_sqlite.h" | ||
| 16 | 20 | ||
| 17 | extern int RunPoolStratumSelfTest(); | 21 | extern int RunPoolStratumSelfTest(); |
| 18 | extern int RunPayoutLedgerSelfTest(); | 22 | extern int RunPayoutLedgerSelfTest(); |
| @@ -35,6 +39,7 @@ extern int RunPayoutLedgerSelfTest(); | |||
| 35 | #undef printf | 39 | #undef printf |
| 36 | 40 | ||
| 37 | #include <mutex> | 41 | #include <mutex> |
| 42 | #include <cerrno> | ||
| 38 | #include <stdexcept> | 43 | #include <stdexcept> |
| 39 | #include <thread> | 44 | #include <thread> |
| 40 | 45 | ||
| @@ -62,6 +67,11 @@ static std::string HexStrLocal(const std::vector<unsigned char>& v) | |||
| 62 | return out; | 67 | return out; |
| 63 | } | 68 | } |
| 64 | 69 | ||
| 70 | static vector<unsigned char> DataStreamBytes(const CDataStream& ss) | ||
| 71 | { | ||
| 72 | return vector<unsigned char>(ss.begin(), ss.end()); | ||
| 73 | } | ||
| 74 | |||
| 65 | static bool MakeTempDir(std::string& out) | 75 | static bool MakeTempDir(std::string& out) |
| 66 | { | 76 | { |
| 67 | #ifdef _WIN32 | 77 | #ifdef _WIN32 |
| @@ -736,6 +746,1017 @@ static int RunWalletFormatSelfTest() | |||
| 736 | return nFail == 0 ? 0 : 1; | 746 | return nFail == 0 ? 0 : 1; |
| 737 | } | 747 | } |
| 738 | 748 | ||
| 749 | static bool FileContainsText(const string& strPath, const string& strNeedle); | ||
| 750 | static bool MakeDirLocal(const string& strPath); | ||
| 751 | static int RunBitflashChild(const string& strExe, | ||
| 752 | const vector<string>& vArgs, | ||
| 753 | const string* pStdinFile, | ||
| 754 | const string* pOutputFile); | ||
| 755 | |||
| 756 | class CWalletStorageSanityRawDB : public CWalletDB | ||
| 757 | { | ||
| 758 | public: | ||
| 759 | CWalletStorageSanityRawDB(const char* pszMode="r+") | ||
| 760 | : CWalletDB(pszMode) { } | ||
| 761 | |||
| 762 | bool WriteUnknownRecord() | ||
| 763 | { | ||
| 764 | return Write(string("storage-sanity-unknown"), 1); | ||
| 765 | } | ||
| 766 | |||
| 767 | bool WriteMalformedKeyRecord() | ||
| 768 | { | ||
| 769 | if (!pdb) | ||
| 770 | return false; | ||
| 771 | unsigned char chKey = 0xff; | ||
| 772 | unsigned char chValue = 0x01; | ||
| 773 | Dbt datKey(&chKey, 1); | ||
| 774 | Dbt datValue(&chValue, 1); | ||
| 775 | return pdb->put(GetTxn(), &datKey, &datValue, 0) == 0; | ||
| 776 | } | ||
| 777 | |||
| 778 | bool MakePlainHDSeedIncomplete() | ||
| 779 | { | ||
| 780 | return Erase(string("hdchaincode")); | ||
| 781 | } | ||
| 782 | |||
| 783 | bool AddEncryptedMarkerToPlainWallet() | ||
| 784 | { | ||
| 785 | CWalletMasterKey kMasterKey; | ||
| 786 | kMasterKey.vchCryptedKey.resize(48); | ||
| 787 | for (size_t i = 0; i < kMasterKey.vchCryptedKey.size(); i++) | ||
| 788 | kMasterKey.vchCryptedKey[i] = (unsigned char)(0x80 + (i & 0x3f)); | ||
| 789 | for (size_t i = 0; i < kMasterKey.vchSalt.size(); i++) | ||
| 790 | kMasterKey.vchSalt[i] = (unsigned char)(0x40 + i); | ||
| 791 | return WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) && | ||
| 792 | WriteMasterKey(1, kMasterKey); | ||
| 793 | } | ||
| 794 | }; | ||
| 795 | |||
| 796 | static const char* SELFTEST_MUTATE_WALLET_MARKER = | ||
| 797 | ".bitflash-selftest-wallet-mutate-ok"; | ||
| 798 | static const char* SELFTEST_MUTATE_WALLET_MARKER_TEXT = | ||
| 799 | "bitflash destructive wallet storage self-test\n"; | ||
| 800 | |||
| 801 | static string SelfTestMutationMarkerPath() | ||
| 802 | { | ||
| 803 | return GetAppDir() + "/" + SELFTEST_MUTATE_WALLET_MARKER; | ||
| 804 | } | ||
| 805 | |||
| 806 | static bool WriteSelfTestMutationMarker(const string& strDir) | ||
| 807 | { | ||
| 808 | string strPath = strDir + "/" + SELFTEST_MUTATE_WALLET_MARKER; | ||
| 809 | FILE* pf = fopen(strPath.c_str(), "wb"); | ||
| 810 | if (!pf) | ||
| 811 | return false; | ||
| 812 | bool fOk = fwrite(SELFTEST_MUTATE_WALLET_MARKER_TEXT, 1, | ||
| 813 | strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT), pf) == | ||
| 814 | strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT); | ||
| 815 | if (fclose(pf) != 0) | ||
| 816 | fOk = false; | ||
| 817 | return fOk; | ||
| 818 | } | ||
| 819 | |||
| 820 | int RunSelfTestMutateWallet(const std::string& name) | ||
| 821 | { | ||
| 822 | if (!FileContainsText(SelfTestMutationMarkerPath(), | ||
| 823 | SELFTEST_MUTATE_WALLET_MARKER_TEXT)) | ||
| 824 | { | ||
| 825 | fprintf(stderr, "Refusing destructive wallet storage mutation outside " | ||
| 826 | "a marked self-test datadir.\n"); | ||
| 827 | return 1; | ||
| 828 | } | ||
| 829 | |||
| 830 | bool fOk = false; | ||
| 831 | try | ||
| 832 | { | ||
| 833 | CWalletStorageSanityRawDB walletdb; | ||
| 834 | if (name == "unknown-record") | ||
| 835 | fOk = walletdb.WriteUnknownRecord(); | ||
| 836 | else if (name == "malformed-key") | ||
| 837 | fOk = walletdb.WriteMalformedKeyRecord(); | ||
| 838 | else if (name == "incomplete-plain-hd") | ||
| 839 | fOk = walletdb.MakePlainHDSeedIncomplete(); | ||
| 840 | else if (name == "encrypted-marker-with-plain-keys") | ||
| 841 | fOk = walletdb.AddEncryptedMarkerToPlainWallet(); | ||
| 842 | else | ||
| 843 | { | ||
| 844 | fprintf(stderr, "Unknown wallet storage mutation '%s'\n", name.c_str()); | ||
| 845 | return 1; | ||
| 846 | } | ||
| 847 | } | ||
| 848 | catch (const std::exception& e) | ||
| 849 | { | ||
| 850 | fprintf(stderr, "Cannot mutate wallet storage: %s\n", e.what()); | ||
| 851 | return 1; | ||
| 852 | } | ||
| 853 | catch (...) | ||
| 854 | { | ||
| 855 | fprintf(stderr, "Cannot mutate wallet storage: unknown exception\n"); | ||
| 856 | return 1; | ||
| 857 | } | ||
| 858 | |||
| 859 | DBFlush(true); | ||
| 860 | return fOk ? 0 : 1; | ||
| 861 | } | ||
| 862 | |||
| 863 | static int RunWalletStorageSanitySelfTest() | ||
| 864 | { | ||
| 865 | fflush(stdout); | ||
| 866 | printf("wallet-storage-sanity self-test\n"); | ||
| 867 | |||
| 868 | std::string tmp; | ||
| 869 | std::string cwd; | ||
| 870 | if (!MakeTempDir(tmp)) | ||
| 871 | { | ||
| 872 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 873 | return 1; | ||
| 874 | } | ||
| 875 | if (!GetCurrentDir(cwd)) | ||
| 876 | { | ||
| 877 | printf(" FAIL could not read current directory\n"); | ||
| 878 | RemoveTree(tmp); | ||
| 879 | return 1; | ||
| 880 | } | ||
| 881 | |||
| 882 | int nFail = 0; | ||
| 883 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 884 | |||
| 885 | try | ||
| 886 | { | ||
| 887 | #ifdef _WIN32 | ||
| 888 | string strExe = cwd + "\\bitflash.exe"; | ||
| 889 | #else | ||
| 890 | string strExe = cwd + "/bitflash-node"; | ||
| 891 | #endif | ||
| 892 | |||
| 893 | struct StorageScenario | ||
| 894 | { | ||
| 895 | const char* pszName; | ||
| 896 | const char* pszExpectedFailure; | ||
| 897 | }; | ||
| 898 | |||
| 899 | StorageScenario scenarios[] = { | ||
| 900 | { "unknown-record", "wallet.dat contains unknown records" }, | ||
| 901 | { "malformed-key", "wallet.dat contains malformed records" }, | ||
| 902 | { "incomplete-plain-hd", "plain HD seed is incomplete" }, | ||
| 903 | { "encrypted-marker-with-plain-keys", | ||
| 904 | "encrypted wallet still contains plain private key records" }, | ||
| 905 | }; | ||
| 906 | |||
| 907 | for (size_t i = 0; i < sizeof(scenarios)/sizeof(scenarios[0]); i++) | ||
| 908 | { | ||
| 909 | string strScenarioDir = tmp + "/" + scenarios[i].pszName; | ||
| 910 | nFail += Check(MakeDirLocal(strScenarioDir), | ||
| 911 | "scenario datadir can be created") ? 0 : 1; | ||
| 912 | |||
| 913 | string strCreateOut = tmp + "/" + scenarios[i].pszName + "-create.txt"; | ||
| 914 | vector<string> vCreateArgs; | ||
| 915 | vCreateArgs.push_back("-datadir=" + strScenarioDir); | ||
| 916 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 917 | vCreateArgs.push_back("-nogui"); | ||
| 918 | vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat | ||
| 919 | vCreateArgs.push_back("-newphrase"); | ||
| 920 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 921 | nFail += Check(nCreateRet == 0 && | ||
| 922 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 923 | "scenario phrase wallet can be created") ? 0 : 1; | ||
| 924 | |||
| 925 | if (i == 0) | ||
| 926 | { | ||
| 927 | string strBlockedOut = tmp + "/" + scenarios[i].pszName + "-blocked-mutate.txt"; | ||
| 928 | vector<string> vBlockedArgs; | ||
| 929 | vBlockedArgs.push_back("-datadir=" + strScenarioDir); | ||
| 930 | vBlockedArgs.push_back("-nomanagedtor"); | ||
| 931 | vBlockedArgs.push_back("-nogui"); | ||
| 932 | vBlockedArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); | ||
| 933 | int nBlockedRet = RunBitflashChild(strExe, vBlockedArgs, NULL, &strBlockedOut); | ||
| 934 | nFail += Check(nBlockedRet != 0 && | ||
| 935 | FileContainsText(strBlockedOut, "Refusing destructive wallet storage mutation"), | ||
| 936 | "wallet mutation helper is blocked without marker") ? 0 : 1; | ||
| 937 | } | ||
| 938 | |||
| 939 | nFail += Check(WriteSelfTestMutationMarker(strScenarioDir), | ||
| 940 | "scenario mutation marker can be written") ? 0 : 1; | ||
| 941 | |||
| 942 | string strMutateOut = tmp + "/" + scenarios[i].pszName + "-mutate.txt"; | ||
| 943 | vector<string> vMutateArgs; | ||
| 944 | vMutateArgs.push_back("-datadir=" + strScenarioDir); | ||
| 945 | vMutateArgs.push_back("-nomanagedtor"); | ||
| 946 | vMutateArgs.push_back("-nogui"); | ||
| 947 | vMutateArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); | ||
| 948 | int nMutateRet = RunBitflashChild(strExe, vMutateArgs, NULL, &strMutateOut); | ||
| 949 | nFail += Check(nMutateRet == 0, scenarios[i].pszName) ? 0 : 1; | ||
| 950 | |||
| 951 | string strCheckOut = tmp + "/" + scenarios[i].pszName + "-check.txt"; | ||
| 952 | vector<string> vCheckArgs; | ||
| 953 | vCheckArgs.push_back("-datadir=" + strScenarioDir); | ||
| 954 | vCheckArgs.push_back("-nomanagedtor"); | ||
| 955 | vCheckArgs.push_back("-nogui"); | ||
| 956 | vCheckArgs.push_back("-walletstoragecheck"); | ||
| 957 | int nCheckRet = RunBitflashChild(strExe, vCheckArgs, NULL, &strCheckOut); | ||
| 958 | nFail += Check(nCheckRet == 2 && | ||
| 959 | FileContainsText(strCheckOut, "storage sanity: failed") && | ||
| 960 | FileContainsText(strCheckOut, scenarios[i].pszExpectedFailure), | ||
| 961 | scenarios[i].pszExpectedFailure) ? 0 : 1; | ||
| 962 | } | ||
| 963 | } | ||
| 964 | catch (const std::exception& e) | ||
| 965 | { | ||
| 966 | printf(" FAIL exception: %s\n", e.what()); | ||
| 967 | nFail++; | ||
| 968 | } | ||
| 969 | catch (...) | ||
| 970 | { | ||
| 971 | printf(" FAIL unknown exception\n"); | ||
| 972 | nFail++; | ||
| 973 | } | ||
| 974 | |||
| 975 | RemoveTree(tmp); | ||
| 976 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 977 | nFail, nFail == 1 ? "" : "s"); | ||
| 978 | fflush(stdout); | ||
| 979 | return nFail == 0 ? 0 : 1; | ||
| 980 | } | ||
| 981 | |||
| 982 | static int RunDbEnvReopenSelfTest() | ||
| 983 | { | ||
| 984 | fflush(stdout); | ||
| 985 | printf("db-env-reopen self-test\n"); | ||
| 986 | |||
| 987 | std::string tmp; | ||
| 988 | if (!MakeTempDir(tmp)) | ||
| 989 | { | ||
| 990 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 991 | return 1; | ||
| 992 | } | ||
| 993 | |||
| 994 | int nFail = 0; | ||
| 995 | std::string strOldDataDir = strSetDataDir; | ||
| 996 | strSetDataDir = tmp; | ||
| 997 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 998 | |||
| 999 | try | ||
| 1000 | { | ||
| 1001 | for (int i = 0; i < 5; i++) | ||
| 1002 | { | ||
| 1003 | int64 nValue = 1000 + i; | ||
| 1004 | { | ||
| 1005 | CWalletDB walletdb("cr+"); | ||
| 1006 | nFail += Check(walletdb.WriteSetting("db-env-reopen-cycle", nValue), | ||
| 1007 | "wallet setting can be written before shutdown") ? 0 : 1; | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | DBFlush(true); | ||
| 1011 | |||
| 1012 | int64 nRead = 0; | ||
| 1013 | { | ||
| 1014 | CWalletDB walletdb("r"); | ||
| 1015 | nFail += Check(walletdb.ReadSetting("db-env-reopen-cycle", nRead), | ||
| 1016 | "wallet setting can be read after env reopen") ? 0 : 1; | ||
| 1017 | } | ||
| 1018 | nFail += Check(nRead == nValue, | ||
| 1019 | "reopened environment reads the last committed value") ? 0 : 1; | ||
| 1020 | |||
| 1021 | DBFlush(true); | ||
| 1022 | } | ||
| 1023 | } | ||
| 1024 | catch (const std::exception& e) | ||
| 1025 | { | ||
| 1026 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1027 | nFail++; | ||
| 1028 | } | ||
| 1029 | catch (...) | ||
| 1030 | { | ||
| 1031 | printf(" FAIL unknown exception\n"); | ||
| 1032 | nFail++; | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | DBFlush(true); | ||
| 1036 | strSetDataDir = strOldDataDir; | ||
| 1037 | RemoveTree(tmp); | ||
| 1038 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1039 | nFail, nFail == 1 ? "" : "s"); | ||
| 1040 | fflush(stdout); | ||
| 1041 | return nFail == 0 ? 0 : 1; | ||
| 1042 | } | ||
| 1043 | |||
| 1044 | static int RunWalletSQLiteSelfTest() | ||
| 1045 | { | ||
| 1046 | fflush(stdout); | ||
| 1047 | printf("wallet-sqlite self-test\n"); | ||
| 1048 | |||
| 1049 | std::string tmp; | ||
| 1050 | if (!MakeTempDir(tmp)) | ||
| 1051 | { | ||
| 1052 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 1053 | return 1; | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | int nFail = 0; | ||
| 1057 | string strPath = tmp + "/wallet.sqlite"; | ||
| 1058 | string strError; | ||
| 1059 | string strOldRuntimeDataDir; | ||
| 1060 | bool fRestoreRuntimeDataDir = false; | ||
| 1061 | |||
| 1062 | try | ||
| 1063 | { | ||
| 1064 | CDataStream ssKeySetting(SER_DISK); | ||
| 1065 | ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip")); | ||
| 1066 | CDataStream ssValueSetting(SER_DISK); | ||
| 1067 | ssValueSetting << (int64)424242; | ||
| 1068 | |||
| 1069 | vector<unsigned char> vchPubKey; | ||
| 1070 | vchPubKey.push_back(0x04); | ||
| 1071 | for (int i = 0; i < 64; i++) | ||
| 1072 | vchPubKey.push_back((unsigned char)i); | ||
| 1073 | CDataStream ssKeyDefault(SER_DISK); | ||
| 1074 | ssKeyDefault << string("defaultkey"); | ||
| 1075 | CDataStream ssValueDefault(SER_DISK); | ||
| 1076 | ssValueDefault << vchPubKey; | ||
| 1077 | |||
| 1078 | { | ||
| 1079 | CWalletDBSQLite db; | ||
| 1080 | nFail += Check(db.Open(strPath, strError), | ||
| 1081 | "SQLite wallet opens and creates schema") ? 0 : 1; | ||
| 1082 | nFail += Check(db.BeginTransaction(strError), | ||
| 1083 | "SQLite wallet begins a write transaction") ? 0 : 1; | ||
| 1084 | nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting), | ||
| 1085 | DataStreamBytes(ssValueSetting), | ||
| 1086 | strError), | ||
| 1087 | "SQLite wallet writes a serialized setting record") ? 0 : 1; | ||
| 1088 | nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault), | ||
| 1089 | DataStreamBytes(ssValueDefault), | ||
| 1090 | strError), | ||
| 1091 | "SQLite wallet writes a serialized defaultkey record") ? 0 : 1; | ||
| 1092 | nFail += Check(db.CommitTransaction(strError), | ||
| 1093 | "SQLite wallet commits a write transaction") ? 0 : 1; | ||
| 1094 | int nCount = 0; | ||
| 1095 | nFail += Check(db.CountRecords(nCount, strError) && nCount == 2, | ||
| 1096 | "SQLite wallet counts raw records") ? 0 : 1; | ||
| 1097 | } | ||
| 1098 | |||
| 1099 | { | ||
| 1100 | CWalletDBSQLite db; | ||
| 1101 | nFail += Check(db.Open(strPath, strError), | ||
| 1102 | "SQLite wallet reopens") ? 0 : 1; | ||
| 1103 | vector<unsigned char> vchRead; | ||
| 1104 | nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting), | ||
| 1105 | vchRead, | ||
| 1106 | strError), | ||
| 1107 | "SQLite wallet reads a serialized setting record") ? 0 : 1; | ||
| 1108 | int64 nReadValue = 0; | ||
| 1109 | CDataStream ssRead(vchRead, SER_DISK); | ||
| 1110 | ssRead >> nReadValue; | ||
| 1111 | nFail += Check(!ssRead.fail() && nReadValue == 424242, | ||
| 1112 | "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1; | ||
| 1113 | |||
| 1114 | vchRead.clear(); | ||
| 1115 | nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault), | ||
| 1116 | vchRead, | ||
| 1117 | strError), | ||
| 1118 | "SQLite wallet reads a serialized defaultkey record") ? 0 : 1; | ||
| 1119 | vector<unsigned char> vchReadPubKey; | ||
| 1120 | CDataStream ssReadDefault(vchRead, SER_DISK); | ||
| 1121 | ssReadDefault >> vchReadPubKey; | ||
| 1122 | nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey, | ||
| 1123 | "SQLite wallet preserves public key blobs") ? 0 : 1; | ||
| 1124 | } | ||
| 1125 | |||
| 1126 | { | ||
| 1127 | CWalletDBSQLite db; | ||
| 1128 | nFail += Check(db.Open(strPath, strError), | ||
| 1129 | "SQLite wallet reopens for typed records") ? 0 : 1; | ||
| 1130 | |||
| 1131 | nFail += Check(db.WriteTypedRecord(make_pair(string("setting"), | ||
| 1132 | string("sqlite-typed")), | ||
| 1133 | (int64)777, strError), | ||
| 1134 | "SQLite wallet writes a typed setting record") ? 0 : 1; | ||
| 1135 | |||
| 1136 | int64 nTypedRead = 0; | ||
| 1137 | nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1138 | string("sqlite-typed")), | ||
| 1139 | nTypedRead, strError) && | ||
| 1140 | nTypedRead == 777, | ||
| 1141 | "SQLite wallet reads a typed setting record") ? 0 : 1; | ||
| 1142 | |||
| 1143 | nFail += Check(!db.WriteTypedRecord(make_pair(string("setting"), | ||
| 1144 | string("sqlite-typed")), | ||
| 1145 | (int64)888, strError, false), | ||
| 1146 | "SQLite wallet refuses typed overwrite when asked") ? 0 : 1; | ||
| 1147 | |||
| 1148 | nTypedRead = 0; | ||
| 1149 | nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1150 | string("sqlite-typed")), | ||
| 1151 | nTypedRead, strError) && | ||
| 1152 | nTypedRead == 777, | ||
| 1153 | "SQLite wallet preserves the old typed value after overwrite refusal") ? 0 : 1; | ||
| 1154 | |||
| 1155 | nFail += Check(db.EraseTypedRecord(make_pair(string("setting"), | ||
| 1156 | string("sqlite-typed")), | ||
| 1157 | strError), | ||
| 1158 | "SQLite wallet erases a typed record") ? 0 : 1; | ||
| 1159 | |||
| 1160 | nTypedRead = 0; | ||
| 1161 | nFail += Check(!db.ReadTypedRecord(make_pair(string("setting"), | ||
| 1162 | string("sqlite-typed")), | ||
| 1163 | nTypedRead, strError), | ||
| 1164 | "SQLite wallet typed erase removes the record") ? 0 : 1; | ||
| 1165 | |||
| 1166 | nFail += Check(db.WriteTypedRecord(string("defaultkey"), vchPubKey, | ||
| 1167 | strError), | ||
| 1168 | "SQLite wallet writes a typed defaultkey record") ? 0 : 1; | ||
| 1169 | |||
| 1170 | vector<unsigned char> vchTypedPubKey; | ||
| 1171 | nFail += Check(db.ReadTypedRecord(string("defaultkey"), vchTypedPubKey, | ||
| 1172 | strError) && | ||
| 1173 | vchTypedPubKey == vchPubKey, | ||
| 1174 | "SQLite wallet reads a typed defaultkey record") ? 0 : 1; | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | { | ||
| 1178 | string strRuntimePath = tmp + "/runtime-wallet.sqlite"; | ||
| 1179 | string strRuntimeDir = tmp + "/runtime-datadir"; | ||
| 1180 | nFail += Check(MakeDirLocal(strRuntimeDir), | ||
| 1181 | "SQLite runtime datadir can be created") ? 0 : 1; | ||
| 1182 | |||
| 1183 | { | ||
| 1184 | CWalletDBSQLite db; | ||
| 1185 | nFail += Check(db.Open(strRuntimePath, strError), | ||
| 1186 | "SQLite runtime wallet file can be created") ? 0 : 1; | ||
| 1187 | } | ||
| 1188 | |||
| 1189 | string strOldDataDir = strSetDataDir; | ||
| 1190 | strSetDataDir = strRuntimeDir; | ||
| 1191 | strOldRuntimeDataDir = strOldDataDir; | ||
| 1192 | fRestoreRuntimeDataDir = true; | ||
| 1193 | bool fRuntimeOpened = WalletSQLiteRuntimeOpen(strRuntimePath, strError); | ||
| 1194 | nFail += Check(fRuntimeOpened, | ||
| 1195 | "SQLite runtime wallet can be activated") ? 0 : 1; | ||
| 1196 | |||
| 1197 | if (fRuntimeOpened) | ||
| 1198 | { | ||
| 1199 | CKey key; | ||
| 1200 | key.MakeNewKey(); | ||
| 1201 | vector<unsigned char> vchRuntimePubKey = key.GetPubKey(); | ||
| 1202 | CPrivKey vchRuntimePrivKey = key.GetPrivKey(); | ||
| 1203 | string strRuntimeAddress = PubKeyToAddress(vchRuntimePubKey); | ||
| 1204 | |||
| 1205 | nFail += Check(CWalletDB().WriteSetting("runtime-sqlite-setting", | ||
| 1206 | (int64)909090) && | ||
| 1207 | CWalletDB().WriteDefaultKey(vchRuntimePubKey) && | ||
| 1208 | CWalletDB().WriteKey(vchRuntimePubKey, | ||
| 1209 | vchRuntimePrivKey) && | ||
| 1210 | CWalletDB().WriteName(strRuntimeAddress, | ||
| 1211 | "SQLite Runtime Address"), | ||
| 1212 | "CWalletDB writes route to active SQLite runtime") ? 0 : 1; | ||
| 1213 | |||
| 1214 | int64 nRuntimeSetting = 0; | ||
| 1215 | vector<unsigned char> vchRuntimeDefaultKey; | ||
| 1216 | CPrivKey vchRuntimeReadPrivKey; | ||
| 1217 | string strRuntimeName; | ||
| 1218 | nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", | ||
| 1219 | nRuntimeSetting) && | ||
| 1220 | nRuntimeSetting == 909090 && | ||
| 1221 | CWalletDB("r").ReadDefaultKey(vchRuntimeDefaultKey) && | ||
| 1222 | vchRuntimeDefaultKey == vchRuntimePubKey && | ||
| 1223 | CWalletDB("r").ReadKey(vchRuntimePubKey, | ||
| 1224 | vchRuntimeReadPrivKey) && | ||
| 1225 | vchRuntimeReadPrivKey == vchRuntimePrivKey && | ||
| 1226 | CWalletDB("r").ReadName(strRuntimeAddress, | ||
| 1227 | strRuntimeName) && | ||
| 1228 | strRuntimeName == "SQLite Runtime Address", | ||
| 1229 | "CWalletDB reads route to active SQLite runtime") ? 0 : 1; | ||
| 1230 | |||
| 1231 | nFail += Check(CWalletDB().EraseName(strRuntimeAddress) && | ||
| 1232 | !CWalletDB("r").ReadName(strRuntimeAddress, | ||
| 1233 | strRuntimeName), | ||
| 1234 | "CWalletDB erases route to active SQLite runtime") ? 0 : 1; | ||
| 1235 | |||
| 1236 | nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), | ||
| 1237 | "active SQLite runtime does not create wallet.dat") ? 0 : 1; | ||
| 1238 | |||
| 1239 | // Transaction bracket: a rolled-back group leaves nothing, a | ||
| 1240 | // committed group persists. This is the all-or-nothing that | ||
| 1241 | // WriteHDMaster relies on so a half-written seed cannot survive | ||
| 1242 | // a failure between its two records. | ||
| 1243 | bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); | ||
| 1244 | bool fTxnWrote = fTxnBegan && | ||
| 1245 | CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); | ||
| 1246 | if (fTxnBegan) | ||
| 1247 | WalletSQLiteRuntimeRollbackTxn(); | ||
| 1248 | nFail += Check(fTxnBegan && fTxnWrote, | ||
| 1249 | "a SQLite runtime transaction opens and rolls back") ? 0 : 1; | ||
| 1250 | int64 nRolledBack = 0; | ||
| 1251 | nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", | ||
| 1252 | nRolledBack), | ||
| 1253 | "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; | ||
| 1254 | |||
| 1255 | bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && | ||
| 1256 | CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && | ||
| 1257 | WalletSQLiteRuntimeCommitTxn(); | ||
| 1258 | int64 nCommitted = 0; | ||
| 1259 | nFail += Check(fCommitOk && | ||
| 1260 | CWalletDB("r").ReadSetting("runtime-txn-commit", | ||
| 1261 | nCommitted) && | ||
| 1262 | nCommitted == 222, | ||
| 1263 | "a committed SQLite runtime write persists") ? 0 : 1; | ||
| 1264 | |||
| 1265 | // WriteHDMaster brackets its two records in that transaction; | ||
| 1266 | // both halves of the seed must land. | ||
| 1267 | vector<unsigned char> vchTxnMaster(32, 0x5a); | ||
| 1268 | vector<unsigned char> vchTxnChainCode(32, 0xa5); | ||
| 1269 | vector<unsigned char> vchReadMaster, vchReadChainCode; | ||
| 1270 | nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && | ||
| 1271 | CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && | ||
| 1272 | vchReadMaster == vchTxnMaster && | ||
| 1273 | vchReadChainCode == vchTxnChainCode, | ||
| 1274 | "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1; | ||
| 1275 | |||
| 1276 | // Durability: close the backend (checkpoints the WAL), reopen | ||
| 1277 | // the same file, and confirm what CWalletDB wrote is still | ||
| 1278 | // there. This is the write-survives-a-restart property the real | ||
| 1279 | // -walletbackend=sqlite node depends on. | ||
| 1280 | WalletSQLiteRuntimeClose(); | ||
| 1281 | bool fReopened = | ||
| 1282 | WalletSQLiteRuntimeOpen(strRuntimePath, strError); | ||
| 1283 | nFail += Check(fReopened, | ||
| 1284 | "SQLite runtime wallet reopens after close") ? 0 : 1; | ||
| 1285 | if (fReopened) | ||
| 1286 | { | ||
| 1287 | int64 nPersisted = 0; | ||
| 1288 | CPrivKey vchPersistedPrivKey; | ||
| 1289 | vector<unsigned char> vchPersistedDefaultKey; | ||
| 1290 | nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", | ||
| 1291 | nPersisted) && | ||
| 1292 | nPersisted == 909090 && | ||
| 1293 | CWalletDB("r").ReadDefaultKey(vchPersistedDefaultKey) && | ||
| 1294 | vchPersistedDefaultKey == vchRuntimePubKey && | ||
| 1295 | CWalletDB("r").ReadKey(vchRuntimePubKey, | ||
| 1296 | vchPersistedPrivKey) && | ||
| 1297 | vchPersistedPrivKey == vchRuntimePrivKey, | ||
| 1298 | "SQLite runtime writes persist across a close and reopen") ? 0 : 1; | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | WalletSQLiteRuntimeClose(); | ||
| 1303 | strSetDataDir = strOldDataDir; | ||
| 1304 | fRestoreRuntimeDataDir = false; | ||
| 1305 | } | ||
| 1306 | } | ||
| 1307 | catch (const std::exception& e) | ||
| 1308 | { | ||
| 1309 | WalletSQLiteRuntimeClose(); | ||
| 1310 | if (fRestoreRuntimeDataDir) | ||
| 1311 | strSetDataDir = strOldRuntimeDataDir; | ||
| 1312 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1313 | nFail++; | ||
| 1314 | } | ||
| 1315 | catch (...) | ||
| 1316 | { | ||
| 1317 | WalletSQLiteRuntimeClose(); | ||
| 1318 | if (fRestoreRuntimeDataDir) | ||
| 1319 | strSetDataDir = strOldRuntimeDataDir; | ||
| 1320 | printf(" FAIL unknown exception\n"); | ||
| 1321 | nFail++; | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | RemoveTree(tmp); | ||
| 1325 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1326 | nFail, nFail == 1 ? "" : "s"); | ||
| 1327 | fflush(stdout); | ||
| 1328 | return nFail == 0 ? 0 : 1; | ||
| 1329 | } | ||
| 1330 | |||
| 1331 | namespace { | ||
| 1332 | |||
| 1333 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 1334 | { | ||
| 1335 | public: | ||
| 1336 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords; | ||
| 1337 | |||
| 1338 | explicit CWalletRecordMapVisitor( | ||
| 1339 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn) | ||
| 1340 | : mapRecords(mapRecordsIn) { } | ||
| 1341 | |||
| 1342 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 1343 | const CDataStream& ssValue, | ||
| 1344 | string& strErrorRet) | ||
| 1345 | { | ||
| 1346 | vector<unsigned char> vchKey = DataStreamBytes(ssKey); | ||
| 1347 | if (mapRecords.count(vchKey)) | ||
| 1348 | { | ||
| 1349 | strErrorRet = "duplicate serialized wallet key"; | ||
| 1350 | return false; | ||
| 1351 | } | ||
| 1352 | mapRecords[vchKey] = DataStreamBytes(ssValue); | ||
| 1353 | return true; | ||
| 1354 | } | ||
| 1355 | }; | ||
| 1356 | |||
| 1357 | } // namespace | ||
| 1358 | |||
| 1359 | static bool CreateSQLiteWalletWithEmptyKey(const string& strPath) | ||
| 1360 | { | ||
| 1361 | sqlite3* pdb = NULL; | ||
| 1362 | int ret = sqlite3_open_v2(strPath.c_str(), &pdb, | ||
| 1363 | SQLITE_OPEN_READWRITE | | ||
| 1364 | SQLITE_OPEN_CREATE | | ||
| 1365 | SQLITE_OPEN_FULLMUTEX, | ||
| 1366 | NULL); | ||
| 1367 | if (ret != SQLITE_OK || !pdb) | ||
| 1368 | { | ||
| 1369 | if (pdb) | ||
| 1370 | sqlite3_close(pdb); | ||
| 1371 | return false; | ||
| 1372 | } | ||
| 1373 | |||
| 1374 | char* pszErr = NULL; | ||
| 1375 | bool fOk = | ||
| 1376 | sqlite3_exec(pdb, | ||
| 1377 | "CREATE TABLE wallet_records (" | ||
| 1378 | "key BLOB PRIMARY KEY NOT NULL," | ||
| 1379 | "value BLOB NOT NULL" | ||
| 1380 | ");", | ||
| 1381 | NULL, NULL, &pszErr) == SQLITE_OK && | ||
| 1382 | sqlite3_exec(pdb, | ||
| 1383 | "INSERT INTO wallet_records(key, value) " | ||
| 1384 | "VALUES(x'', x'01');", | ||
| 1385 | NULL, NULL, &pszErr) == SQLITE_OK; | ||
| 1386 | if (pszErr) | ||
| 1387 | sqlite3_free(pszErr); | ||
| 1388 | sqlite3_close(pdb); | ||
| 1389 | return fOk; | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath) | ||
| 1393 | { | ||
| 1394 | string strError; | ||
| 1395 | CWalletDBSQLite db; | ||
| 1396 | CDataStream ssKey(SER_DISK); | ||
| 1397 | ssKey << string("walletminversion"); | ||
| 1398 | CDataStream ssValue(SER_DISK); | ||
| 1399 | ssValue << (WALLET_FORMAT_SUPPORTED + 1); | ||
| 1400 | |||
| 1401 | if (!db.Open(strPath, strError)) | ||
| 1402 | return false; | ||
| 1403 | if (!db.BeginTransaction(strError)) | ||
| 1404 | return false; | ||
| 1405 | if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError)) | ||
| 1406 | return false; | ||
| 1407 | if (!db.CommitTransaction(strError)) | ||
| 1408 | return false; | ||
| 1409 | if (!db.Checkpoint(strError)) | ||
| 1410 | return false; | ||
| 1411 | return true; | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | static bool CreateTypedSQLiteWallet(const string& strPath) | ||
| 1415 | { | ||
| 1416 | string strError; | ||
| 1417 | CWalletDBSQLite db; | ||
| 1418 | CKey key; | ||
| 1419 | key.MakeNewKey(); | ||
| 1420 | |||
| 1421 | vector<unsigned char> vchPubKey = key.GetPubKey(); | ||
| 1422 | CPrivKey vchPrivKey = key.GetPrivKey(); | ||
| 1423 | string strAddress = PubKeyToAddress(vchPubKey); | ||
| 1424 | |||
| 1425 | if (!db.Open(strPath, strError)) | ||
| 1426 | return false; | ||
| 1427 | if (!db.BeginTransaction(strError)) | ||
| 1428 | return false; | ||
| 1429 | if (!db.WriteTypedRecord(make_pair(string("key"), vchPubKey), | ||
| 1430 | vchPrivKey, strError, false)) | ||
| 1431 | return false; | ||
| 1432 | if (!db.WriteTypedRecord(string("defaultkey"), vchPubKey, strError)) | ||
| 1433 | return false; | ||
| 1434 | if (!db.WriteTypedRecord(make_pair(string("name"), strAddress), | ||
| 1435 | string("Typed SQLite Address"), strError)) | ||
| 1436 | return false; | ||
| 1437 | if (!db.CommitTransaction(strError)) | ||
| 1438 | return false; | ||
| 1439 | if (!db.Checkpoint(strError)) | ||
| 1440 | return false; | ||
| 1441 | return true; | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | static int RunWalletSQLiteMigrationSelfTest() | ||
| 1445 | { | ||
| 1446 | fflush(stdout); | ||
| 1447 | printf("wallet-sqlite-migration self-test\n"); | ||
| 1448 | |||
| 1449 | std::string tmp; | ||
| 1450 | std::string cwd; | ||
| 1451 | if (!MakeTempDir(tmp)) | ||
| 1452 | { | ||
| 1453 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 1454 | return 1; | ||
| 1455 | } | ||
| 1456 | if (!GetCurrentDir(cwd)) | ||
| 1457 | { | ||
| 1458 | printf(" FAIL could not read current directory\n"); | ||
| 1459 | RemoveTree(tmp); | ||
| 1460 | return 1; | ||
| 1461 | } | ||
| 1462 | |||
| 1463 | int nFail = 0; | ||
| 1464 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 1465 | |||
| 1466 | try | ||
| 1467 | { | ||
| 1468 | #ifdef _WIN32 | ||
| 1469 | string strExe = cwd + "\\bitflash.exe"; | ||
| 1470 | #else | ||
| 1471 | string strExe = cwd + "/bitflash-node"; | ||
| 1472 | #endif | ||
| 1473 | string strWalletDir = tmp + "/wallet"; | ||
| 1474 | nFail += Check(MakeDirLocal(strWalletDir), | ||
| 1475 | "temporary wallet directory can be created") ? 0 : 1; | ||
| 1476 | |||
| 1477 | string strCreateOut = tmp + "/create.txt"; | ||
| 1478 | vector<string> vCreateArgs; | ||
| 1479 | vCreateArgs.push_back("-datadir=" + strWalletDir); | ||
| 1480 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 1481 | vCreateArgs.push_back("-nogui"); | ||
| 1482 | vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat | ||
| 1483 | vCreateArgs.push_back("-newphrase"); | ||
| 1484 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 1485 | nFail += Check(nCreateRet == 0 && | ||
| 1486 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 1487 | "a phrase wallet can be created for SQLite export") ? 0 : 1; | ||
| 1488 | |||
| 1489 | string strSQLite = tmp + "/wallet.sqlite"; | ||
| 1490 | string strExportOut = tmp + "/sqlite-export.txt"; | ||
| 1491 | vector<string> vExportArgs; | ||
| 1492 | vExportArgs.push_back("-datadir=" + strWalletDir); | ||
| 1493 | vExportArgs.push_back("-nomanagedtor"); | ||
| 1494 | vExportArgs.push_back("-nogui"); | ||
| 1495 | vExportArgs.push_back("-walletsqliteexport=" + strSQLite); | ||
| 1496 | int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); | ||
| 1497 | nFail += Check(nExportRet == 0 && | ||
| 1498 | FileExists(strSQLite.c_str()) && | ||
| 1499 | FileContainsText(strExportOut, "SQLite wallet export written"), | ||
| 1500 | "wallet.dat can be exported to SQLite") ? 0 : 1; | ||
| 1501 | |||
| 1502 | string strVerifyOut = tmp + "/sqlite-verify.txt"; | ||
| 1503 | vector<string> vVerifyArgs; | ||
| 1504 | vVerifyArgs.push_back("-datadir=" + strWalletDir); | ||
| 1505 | vVerifyArgs.push_back("-nomanagedtor"); | ||
| 1506 | vVerifyArgs.push_back("-nogui"); | ||
| 1507 | vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite); | ||
| 1508 | int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut); | ||
| 1509 | nFail += Check(nVerifyRet == 0 && | ||
| 1510 | FileContainsText(strVerifyOut, "verification: ok"), | ||
| 1511 | "SQLite wallet export verifies against wallet.dat") ? 0 : 1; | ||
| 1512 | |||
| 1513 | string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt"; | ||
| 1514 | vector<string> vLoadCheckArgs; | ||
| 1515 | vLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1516 | vLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1517 | vLoadCheckArgs.push_back("-nogui"); | ||
| 1518 | vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite); | ||
| 1519 | int nLoadCheckRet = | ||
| 1520 | RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut); | ||
| 1521 | nFail += Check(nLoadCheckRet == 0 && | ||
| 1522 | FileContainsText(strLoadCheckOut, | ||
| 1523 | "load check: ok"), | ||
| 1524 | "SQLite wallet export passes the loader compatibility check") ? 0 : 1; | ||
| 1525 | |||
| 1526 | string strRuntimeOut = tmp + "/sqlite-runtime.txt"; | ||
| 1527 | vector<string> vRuntimeArgs; | ||
| 1528 | vRuntimeArgs.push_back("-datadir=" + strWalletDir); | ||
| 1529 | vRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1530 | vRuntimeArgs.push_back("-nogui"); | ||
| 1531 | vRuntimeArgs.push_back("-walletsqlite=" + strSQLite); | ||
| 1532 | int nRuntimeRet = | ||
| 1533 | RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut); | ||
| 1534 | nFail += Check(nRuntimeRet == 0 && | ||
| 1535 | FileContainsText(strRuntimeOut, | ||
| 1536 | "SQLite wallet loaded: read-only staging backend"), | ||
| 1537 | "SQLite wallet export loads through the runtime startup path") ? 0 : 1; | ||
| 1538 | |||
| 1539 | string strTypedSQLite = tmp + "/typed-wallet.sqlite"; | ||
| 1540 | nFail += Check(CreateTypedSQLiteWallet(strTypedSQLite), | ||
| 1541 | "typed SQLite wallet can be created without Berkeley DB") ? 0 : 1; | ||
| 1542 | |||
| 1543 | string strTypedLoadCheckOut = tmp + "/sqlite-typed-loadcheck.txt"; | ||
| 1544 | vector<string> vTypedLoadCheckArgs; | ||
| 1545 | vTypedLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1546 | vTypedLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1547 | vTypedLoadCheckArgs.push_back("-nogui"); | ||
| 1548 | vTypedLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strTypedSQLite); | ||
| 1549 | int nTypedLoadCheckRet = | ||
| 1550 | RunBitflashChild(strExe, vTypedLoadCheckArgs, NULL, | ||
| 1551 | &strTypedLoadCheckOut); | ||
| 1552 | nFail += Check(nTypedLoadCheckRet == 0 && | ||
| 1553 | FileContainsText(strTypedLoadCheckOut, | ||
| 1554 | "load check: ok"), | ||
| 1555 | "typed SQLite wallet passes the loader compatibility check") ? 0 : 1; | ||
| 1556 | |||
| 1557 | string strTypedRuntimeDir = tmp + "/typed-runtime-wallet"; | ||
| 1558 | nFail += Check(MakeDirLocal(strTypedRuntimeDir), | ||
| 1559 | "typed SQLite runtime directory can be created") ? 0 : 1; | ||
| 1560 | string strTypedRuntimeOut = tmp + "/sqlite-typed-runtime.txt"; | ||
| 1561 | vector<string> vTypedRuntimeArgs; | ||
| 1562 | vTypedRuntimeArgs.push_back("-datadir=" + strTypedRuntimeDir); | ||
| 1563 | vTypedRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1564 | vTypedRuntimeArgs.push_back("-nogui"); | ||
| 1565 | vTypedRuntimeArgs.push_back("-walletsqlite=" + strTypedSQLite); | ||
| 1566 | int nTypedRuntimeRet = | ||
| 1567 | RunBitflashChild(strExe, vTypedRuntimeArgs, NULL, | ||
| 1568 | &strTypedRuntimeOut); | ||
| 1569 | nFail += Check(nTypedRuntimeRet == 0 && | ||
| 1570 | FileContainsText(strTypedRuntimeOut, | ||
| 1571 | "SQLite wallet loaded: read-only staging backend") && | ||
| 1572 | !FileExists((strTypedRuntimeDir + "/wallet.dat").c_str()), | ||
| 1573 | "typed SQLite wallet loads through runtime without creating wallet.dat") ? 0 : 1; | ||
| 1574 | |||
| 1575 | string strRestoredDir = tmp + "/restored-wallet"; | ||
| 1576 | nFail += Check(MakeDirLocal(strRestoredDir), | ||
| 1577 | "restored wallet directory can be created") ? 0 : 1; | ||
| 1578 | string strRestoreOut = tmp + "/sqlite-restore.txt"; | ||
| 1579 | vector<string> vRestoreArgs; | ||
| 1580 | vRestoreArgs.push_back("-datadir=" + strRestoredDir); | ||
| 1581 | vRestoreArgs.push_back("-nomanagedtor"); | ||
| 1582 | vRestoreArgs.push_back("-nogui"); | ||
| 1583 | vRestoreArgs.push_back("-walletsqliterestore=" + strSQLite); | ||
| 1584 | int nRestoreRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOut); | ||
| 1585 | nFail += Check(nRestoreRet == 0 && | ||
| 1586 | FileContainsText(strRestoreOut, "records restored:"), | ||
| 1587 | "SQLite wallet export restores into a fresh wallet.dat") ? 0 : 1; | ||
| 1588 | |||
| 1589 | string strVerifyRestoredOut = tmp + "/sqlite-verify-restored.txt"; | ||
| 1590 | vector<string> vVerifyRestoredArgs; | ||
| 1591 | vVerifyRestoredArgs.push_back("-datadir=" + strRestoredDir); | ||
| 1592 | vVerifyRestoredArgs.push_back("-nomanagedtor"); | ||
| 1593 | vVerifyRestoredArgs.push_back("-nogui"); | ||
| 1594 | vVerifyRestoredArgs.push_back("-walletsqliteverify=" + strSQLite); | ||
| 1595 | int nVerifyRestoredRet = | ||
| 1596 | RunBitflashChild(strExe, vVerifyRestoredArgs, NULL, &strVerifyRestoredOut); | ||
| 1597 | nFail += Check(nVerifyRestoredRet == 0 && | ||
| 1598 | FileContainsText(strVerifyRestoredOut, | ||
| 1599 | "verification: ok"), | ||
| 1600 | "restored wallet.dat verifies against the SQLite export") ? 0 : 1; | ||
| 1601 | |||
| 1602 | string strRestoreOverwriteOut = tmp + "/sqlite-restore-overwrite.txt"; | ||
| 1603 | int nRestoreOverwriteRet = | ||
| 1604 | RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOverwriteOut); | ||
| 1605 | nFail += Check(nRestoreOverwriteRet == 1 && | ||
| 1606 | FileContainsText(strRestoreOverwriteOut, | ||
| 1607 | "Refusing to overwrite existing wallet.dat"), | ||
| 1608 | "SQLite wallet restore refuses to overwrite wallet.dat") ? 0 : 1; | ||
| 1609 | |||
| 1610 | string strBadSQLite = tmp + "/bad-empty-key.sqlite"; | ||
| 1611 | nFail += Check(CreateSQLiteWalletWithEmptyKey(strBadSQLite), | ||
| 1612 | "bad SQLite export with an empty key can be created") ? 0 : 1; | ||
| 1613 | string strBadRestoreDir = tmp + "/bad-restore-wallet"; | ||
| 1614 | nFail += Check(MakeDirLocal(strBadRestoreDir), | ||
| 1615 | "bad restore wallet directory can be created") ? 0 : 1; | ||
| 1616 | string strBadRestoreOut = tmp + "/sqlite-restore-bad.txt"; | ||
| 1617 | vector<string> vBadRestoreArgs; | ||
| 1618 | vBadRestoreArgs.push_back("-datadir=" + strBadRestoreDir); | ||
| 1619 | vBadRestoreArgs.push_back("-nomanagedtor"); | ||
| 1620 | vBadRestoreArgs.push_back("-nogui"); | ||
| 1621 | vBadRestoreArgs.push_back("-walletsqliterestore=" + strBadSQLite); | ||
| 1622 | int nBadRestoreRet = | ||
| 1623 | RunBitflashChild(strExe, vBadRestoreArgs, NULL, &strBadRestoreOut); | ||
| 1624 | nFail += Check(nBadRestoreRet == 1 && | ||
| 1625 | FileContainsText(strBadRestoreOut, | ||
| 1626 | "Cannot restore SQLite wallet export") && | ||
| 1627 | !FileExists((strBadRestoreDir + "/wallet.dat").c_str()), | ||
| 1628 | "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1; | ||
| 1629 | |||
| 1630 | string strFutureSQLite = tmp + "/future-wallet.sqlite"; | ||
| 1631 | nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite), | ||
| 1632 | "SQLite export with a future wallet format can be created") ? 0 : 1; | ||
| 1633 | string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt"; | ||
| 1634 | vector<string> vFutureLoadCheckArgs; | ||
| 1635 | vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir); | ||
| 1636 | vFutureLoadCheckArgs.push_back("-nomanagedtor"); | ||
| 1637 | vFutureLoadCheckArgs.push_back("-nogui"); | ||
| 1638 | vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite); | ||
| 1639 | int nFutureLoadCheckRet = | ||
| 1640 | RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL, | ||
| 1641 | &strFutureLoadCheckOut); | ||
| 1642 | nFail += Check(nFutureLoadCheckRet == 2 && | ||
| 1643 | FileContainsText(strFutureLoadCheckOut, | ||
| 1644 | "unsupported future wallet format") && | ||
| 1645 | FileContainsText(strFutureLoadCheckOut, | ||
| 1646 | "load check: failed"), | ||
| 1647 | "SQLite wallet load check rejects future wallet formats") ? 0 : 1; | ||
| 1648 | |||
| 1649 | string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt"; | ||
| 1650 | vector<string> vFutureRuntimeArgs; | ||
| 1651 | vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir); | ||
| 1652 | vFutureRuntimeArgs.push_back("-nomanagedtor"); | ||
| 1653 | vFutureRuntimeArgs.push_back("-nogui"); | ||
| 1654 | vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite); | ||
| 1655 | int nFutureRuntimeRet = | ||
| 1656 | RunBitflashChild(strExe, vFutureRuntimeArgs, NULL, | ||
| 1657 | &strFutureRuntimeOut); | ||
| 1658 | nFail += Check(nFutureRuntimeRet == 1 && | ||
| 1659 | FileContainsText(strFutureRuntimeOut, | ||
| 1660 | "Cannot open SQLite wallet export") && | ||
| 1661 | FileContainsText(strFutureRuntimeOut, | ||
| 1662 | "newer version of Bitflash"), | ||
| 1663 | "SQLite runtime startup rejects future wallet formats") ? 0 : 1; | ||
| 1664 | |||
| 1665 | string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; | ||
| 1666 | int nOverwriteRet = | ||
| 1667 | RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); | ||
| 1668 | nFail += Check(nOverwriteRet != 0 && | ||
| 1669 | FileContainsText(strOverwriteOut, | ||
| 1670 | "Refusing to overwrite existing SQLite wallet export"), | ||
| 1671 | "SQLite wallet export refuses to overwrite") ? 0 : 1; | ||
| 1672 | remove((strSQLite + "-wal").c_str()); | ||
| 1673 | remove((strSQLite + "-shm").c_str()); | ||
| 1674 | |||
| 1675 | std::map<vector<unsigned char>, vector<unsigned char> > mapBDB; | ||
| 1676 | string strOldDataDir = strSetDataDir; | ||
| 1677 | string strScanError; | ||
| 1678 | strSetDataDir = strWalletDir; | ||
| 1679 | CWalletRecordMapVisitor visitor(mapBDB); | ||
| 1680 | bool fScanOk = ScanWalletRecords(visitor, strScanError); | ||
| 1681 | DBFlush(true); | ||
| 1682 | strSetDataDir = strOldDataDir; | ||
| 1683 | nFail += Check(fScanOk && !mapBDB.empty(), | ||
| 1684 | "wallet.dat records can be scanned for comparison") ? 0 : 1; | ||
| 1685 | |||
| 1686 | CWalletDBSQLite db; | ||
| 1687 | string strError; | ||
| 1688 | nFail += Check(db.Open(strSQLite, strError), | ||
| 1689 | "exported SQLite wallet opens") ? 0 : 1; | ||
| 1690 | int nSQLiteRecords = 0; | ||
| 1691 | nFail += Check(db.CountRecords(nSQLiteRecords, strError) && | ||
| 1692 | nSQLiteRecords == (int)mapBDB.size(), | ||
| 1693 | "SQLite export has the same record count as wallet.dat") ? 0 : 1; | ||
| 1694 | |||
| 1695 | bool fAllMatch = true; | ||
| 1696 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 1697 | mapBDB.begin(); it != mapBDB.end(); ++it) | ||
| 1698 | { | ||
| 1699 | vector<unsigned char> vchSQLiteValue; | ||
| 1700 | if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || | ||
| 1701 | vchSQLiteValue != it->second) | ||
| 1702 | { | ||
| 1703 | fAllMatch = false; | ||
| 1704 | break; | ||
| 1705 | } | ||
| 1706 | } | ||
| 1707 | nFail += Check(fAllMatch, | ||
| 1708 | "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; | ||
| 1709 | |||
| 1710 | std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite; | ||
| 1711 | CWalletRecordMapVisitor sqliteVisitor(mapSQLite); | ||
| 1712 | nFail += Check(db.ScanRecords(sqliteVisitor, strError) && | ||
| 1713 | mapSQLite == mapBDB, | ||
| 1714 | "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; | ||
| 1715 | |||
| 1716 | db.Close(); | ||
| 1717 | CWalletDBSQLite dbCorrupt; | ||
| 1718 | vector<unsigned char> vchBadValue; | ||
| 1719 | vchBadValue.push_back(0xba); | ||
| 1720 | vchBadValue.push_back(0xad); | ||
| 1721 | nFail += Check(!mapBDB.empty() && | ||
| 1722 | dbCorrupt.Open(strSQLite, strError) && | ||
| 1723 | dbCorrupt.BeginTransaction(strError) && | ||
| 1724 | dbCorrupt.WriteRecord(mapBDB.begin()->first, | ||
| 1725 | vchBadValue, | ||
| 1726 | strError) && | ||
| 1727 | dbCorrupt.CommitTransaction(strError) && | ||
| 1728 | dbCorrupt.Checkpoint(strError), | ||
| 1729 | "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1; | ||
| 1730 | dbCorrupt.Close(); | ||
| 1731 | |||
| 1732 | string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt"; | ||
| 1733 | int nVerifyBadRet = | ||
| 1734 | RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut); | ||
| 1735 | nFail += Check(nVerifyBadRet == 2 && | ||
| 1736 | FileContainsText(strVerifyBadOut, | ||
| 1737 | "record value mismatches: 1") && | ||
| 1738 | FileContainsText(strVerifyBadOut, | ||
| 1739 | "verification: failed"), | ||
| 1740 | "SQLite wallet verifier detects a mismatched record value") ? 0 : 1; | ||
| 1741 | } | ||
| 1742 | catch (const std::exception& e) | ||
| 1743 | { | ||
| 1744 | printf(" FAIL exception: %s\n", e.what()); | ||
| 1745 | nFail++; | ||
| 1746 | } | ||
| 1747 | catch (...) | ||
| 1748 | { | ||
| 1749 | printf(" FAIL unknown exception\n"); | ||
| 1750 | nFail++; | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | RemoveTree(tmp); | ||
| 1754 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 1755 | nFail, nFail == 1 ? "" : "s"); | ||
| 1756 | fflush(stdout); | ||
| 1757 | return nFail == 0 ? 0 : 1; | ||
| 1758 | } | ||
| 1759 | |||
| 739 | static void ClearWalletRuntimeForTest() | 1760 | static void ClearWalletRuntimeForTest() |
| 740 | { | 1761 | { |
| 741 | CRITICAL_BLOCK(cs_mapKeys) | 1762 | CRITICAL_BLOCK(cs_mapKeys) |
| @@ -926,6 +1947,90 @@ static bool WriteTextFile(const string& strPath, const string& strText) | |||
| 926 | return fOk; | 1947 | return fOk; |
| 927 | } | 1948 | } |
| 928 | 1949 | ||
| 1950 | static bool MakeDirLocal(const string& strPath) | ||
| 1951 | { | ||
| 1952 | #ifdef _WIN32 | ||
| 1953 | return CreateDirectoryA(strPath.c_str(), NULL) || GetLastError() == ERROR_ALREADY_EXISTS; | ||
| 1954 | #else | ||
| 1955 | return mkdir(strPath.c_str(), 0700) == 0 || errno == EEXIST; | ||
| 1956 | #endif | ||
| 1957 | } | ||
| 1958 | |||
| 1959 | static bool CopyFileLocal(const string& strSrc, const string& strDst) | ||
| 1960 | { | ||
| 1961 | FILE* in = fopen(strSrc.c_str(), "rb"); | ||
| 1962 | if (!in) | ||
| 1963 | return false; | ||
| 1964 | FILE* out = fopen(strDst.c_str(), "wb"); | ||
| 1965 | if (!out) | ||
| 1966 | { | ||
| 1967 | fclose(in); | ||
| 1968 | return false; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | bool fOk = true; | ||
| 1972 | char buf[65536]; | ||
| 1973 | size_t n = 0; | ||
| 1974 | while ((n = fread(buf, 1, sizeof(buf), in)) > 0) | ||
| 1975 | { | ||
| 1976 | if (fwrite(buf, 1, n, out) != n) | ||
| 1977 | { | ||
| 1978 | fOk = false; | ||
| 1979 | break; | ||
| 1980 | } | ||
| 1981 | } | ||
| 1982 | if (ferror(in)) | ||
| 1983 | fOk = false; | ||
| 1984 | if (fclose(out) != 0) | ||
| 1985 | fOk = false; | ||
| 1986 | fclose(in); | ||
| 1987 | return fOk; | ||
| 1988 | } | ||
| 1989 | |||
| 1990 | static bool DirectoryHasFileWithPrefix(const string& strDir, const string& strPrefix) | ||
| 1991 | { | ||
| 1992 | #ifdef _WIN32 | ||
| 1993 | WIN32_FIND_DATAA findData; | ||
| 1994 | string pattern = strDir + "\\*"; | ||
| 1995 | HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); | ||
| 1996 | if (hFind == INVALID_HANDLE_VALUE) | ||
| 1997 | return false; | ||
| 1998 | bool fFound = false; | ||
| 1999 | do | ||
| 2000 | { | ||
| 2001 | if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) | ||
| 2002 | { | ||
| 2003 | string name = findData.cFileName; | ||
| 2004 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 2005 | { | ||
| 2006 | fFound = true; | ||
| 2007 | break; | ||
| 2008 | } | ||
| 2009 | } | ||
| 2010 | } | ||
| 2011 | while (FindNextFileA(hFind, &findData)); | ||
| 2012 | FindClose(hFind); | ||
| 2013 | return fFound; | ||
| 2014 | #else | ||
| 2015 | DIR* dir = opendir(strDir.c_str()); | ||
| 2016 | if (!dir) | ||
| 2017 | return false; | ||
| 2018 | bool fFound = false; | ||
| 2019 | struct dirent* ent; | ||
| 2020 | while ((ent = readdir(dir)) != NULL) | ||
| 2021 | { | ||
| 2022 | string name = ent->d_name; | ||
| 2023 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 2024 | { | ||
| 2025 | fFound = true; | ||
| 2026 | break; | ||
| 2027 | } | ||
| 2028 | } | ||
| 2029 | closedir(dir); | ||
| 2030 | return fFound; | ||
| 2031 | #endif | ||
| 2032 | } | ||
| 2033 | |||
| 929 | static string QuoteCommandArg(const string& str) | 2034 | static string QuoteCommandArg(const string& str) |
| 930 | { | 2035 | { |
| 931 | string out = "\""; | 2036 | string out = "\""; |
| @@ -942,7 +2047,8 @@ static string QuoteCommandArg(const string& str) | |||
| 942 | 2047 | ||
| 943 | static int RunBitflashChild(const string& strExe, | 2048 | static int RunBitflashChild(const string& strExe, |
| 944 | const vector<string>& vArgs, | 2049 | const vector<string>& vArgs, |
| 945 | const string* pStdinFile = NULL) | 2050 | const string* pStdinFile = NULL, |
| 2051 | const string* pOutputFile = NULL) | ||
| 946 | { | 2052 | { |
| 947 | #ifdef _WIN32 | 2053 | #ifdef _WIN32 |
| 948 | vector<char*> argv; | 2054 | vector<char*> argv; |
| @@ -955,8 +2061,16 @@ static int RunBitflashChild(const string& strExe, | |||
| 955 | int nOldErr = _dup(2); | 2061 | int nOldErr = _dup(2); |
| 956 | int nOldIn = _dup(0); | 2062 | int nOldIn = _dup(0); |
| 957 | int nNull = _open("NUL", _O_WRONLY); | 2063 | int nNull = _open("NUL", _O_WRONLY); |
| 2064 | int nOut = -1; | ||
| 958 | int nIn = -1; | 2065 | int nIn = -1; |
| 959 | if (nNull >= 0) | 2066 | if (pOutputFile) |
| 2067 | nOut = _open(pOutputFile->c_str(), _O_WRONLY|_O_CREAT|_O_TRUNC|_O_BINARY, _S_IREAD|_S_IWRITE); | ||
| 2068 | if (nOut >= 0) | ||
| 2069 | { | ||
| 2070 | _dup2(nOut, 1); | ||
| 2071 | _dup2(nOut, 2); | ||
| 2072 | } | ||
| 2073 | else if (nNull >= 0) | ||
| 960 | { | 2074 | { |
| 961 | _dup2(nNull, 1); | 2075 | _dup2(nNull, 1); |
| 962 | _dup2(nNull, 2); | 2076 | _dup2(nNull, 2); |
| @@ -970,6 +2084,8 @@ static int RunBitflashChild(const string& strExe, | |||
| 970 | int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); | 2084 | int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); |
| 971 | if (nIn >= 0) | 2085 | if (nIn >= 0) |
| 972 | _close(nIn); | 2086 | _close(nIn); |
| 2087 | if (nOut >= 0) | ||
| 2088 | _close(nOut); | ||
| 973 | if (nNull >= 0) | 2089 | if (nNull >= 0) |
| 974 | _close(nNull); | 2090 | _close(nNull); |
| 975 | if (nOldIn >= 0) | 2091 | if (nOldIn >= 0) |
| @@ -994,8 +2110,17 @@ static int RunBitflashChild(const string& strExe, | |||
| 994 | strCmd += " " + QuoteCommandArg(vArgs[i]); | 2110 | strCmd += " " + QuoteCommandArg(vArgs[i]); |
| 995 | if (pStdinFile) | 2111 | if (pStdinFile) |
| 996 | strCmd += " < " + QuoteCommandArg(*pStdinFile); | 2112 | strCmd += " < " + QuoteCommandArg(*pStdinFile); |
| 997 | strCmd += " > /dev/null 2>&1"; | 2113 | if (pOutputFile) |
| 998 | return system(strCmd.c_str()); | 2114 | strCmd += " > " + QuoteCommandArg(*pOutputFile) + " 2>&1"; |
| 2115 | else | ||
| 2116 | strCmd += " > /dev/null 2>&1"; | ||
| 2117 | // system() returns a wait status, not the exit code; unwrap it so callers | ||
| 2118 | // see the same value the Windows _spawnv path returns (e.g. exit code 2 is | ||
| 2119 | // 2, not 512). Without this every "== 2" assertion fails only on Linux. | ||
| 2120 | int nSysRet = system(strCmd.c_str()); | ||
| 2121 | if (nSysRet != -1 && WIFEXITED(nSysRet)) | ||
| 2122 | return WEXITSTATUS(nSysRet); | ||
| 2123 | return nSysRet; | ||
| 999 | #endif | 2124 | #endif |
| 1000 | } | 2125 | } |
| 1001 | 2126 | ||
| @@ -1036,6 +2161,13 @@ static int RunWalletEncryptSelfTest() | |||
| 1036 | TopUpKeyPool(); | 2161 | TopUpKeyPool(); |
| 1037 | 2162 | ||
| 1038 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | 2163 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); |
| 2164 | CKey publicLabelKey; | ||
| 2165 | publicLabelKey.MakeNewKey(); | ||
| 2166 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2167 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2168 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2169 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2170 | |||
| 1039 | CPrivKey vchDefaultPrivKey; | 2171 | CPrivKey vchDefaultPrivKey; |
| 1040 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | 2172 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), |
| 1041 | "the default private key is readable before encryption") ? 0 : 1; | 2173 | "the default private key is readable before encryption") ? 0 : 1; |
| @@ -1063,16 +2195,31 @@ static int RunWalletEncryptSelfTest() | |||
| 1063 | #else | 2195 | #else |
| 1064 | string strExe = cwd + "/bitflash-node"; | 2196 | string strExe = cwd + "/bitflash-node"; |
| 1065 | #endif | 2197 | #endif |
| 2198 | string strStorageAudit = tmp + "/encrypted-storage-audit.txt"; | ||
| 1066 | string strWrongDump = tmp + "/wrong-pass-dump.txt"; | 2199 | string strWrongDump = tmp + "/wrong-pass-dump.txt"; |
| 1067 | string strRightDump = tmp + "/right-pass-dump.txt"; | 2200 | string strRightDump = tmp + "/right-pass-dump.txt"; |
| 1068 | string strLiteralDump = tmp + "/literal-pass-dump.txt"; | 2201 | string strLiteralDump = tmp + "/literal-pass-dump.txt"; |
| 1069 | string strStdinDump = tmp + "/stdin-pass-dump.txt"; | 2202 | string strStdinDump = tmp + "/stdin-pass-dump.txt"; |
| 2203 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2204 | string strUnlockedAudit = tmp + "/unlocked-recovery-audit.txt"; | ||
| 1070 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | 2205 | string strWrongPassFile = tmp + "/wrong-pass.txt"; |
| 1071 | string strRightPassFile = tmp + "/right-pass.txt"; | 2206 | string strRightPassFile = tmp + "/right-pass.txt"; |
| 1072 | nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && | 2207 | nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && |
| 1073 | WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), | 2208 | WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), |
| 1074 | "passphrase files can be written for restarted commands") ? 0 : 1; | 2209 | "passphrase files can be written for restarted commands") ? 0 : 1; |
| 1075 | 2210 | ||
| 2211 | vector<string> vStorageAuditArgs; | ||
| 2212 | vStorageAuditArgs.push_back("-datadir=" + tmp); | ||
| 2213 | vStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2214 | vStorageAuditArgs.push_back("-nogui"); | ||
| 2215 | int nStorageAuditRet = RunBitflashChild(strExe, vStorageAuditArgs, NULL, &strStorageAudit); | ||
| 2216 | nFail += Check(nStorageAuditRet == 0 && | ||
| 2217 | FileContainsText(strStorageAudit, "address book labels: 3"), | ||
| 2218 | "the encrypted rewrite keeps address-book records") ? 0 : 1; | ||
| 2219 | nFail += Check(nStorageAuditRet == 0 && | ||
| 2220 | FileContainsText(strStorageAudit, "settings: 1"), | ||
| 2221 | "the encrypted rewrite keeps wallet setting records") ? 0 : 1; | ||
| 2222 | |||
| 1076 | vector<string> vWrongArgs; | 2223 | vector<string> vWrongArgs; |
| 1077 | vWrongArgs.push_back("-datadir=" + tmp); | 2224 | vWrongArgs.push_back("-datadir=" + tmp); |
| 1078 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | 2225 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); |
| @@ -1082,6 +2229,17 @@ static int RunWalletEncryptSelfTest() | |||
| 1082 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | 2229 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), |
| 1083 | "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; | 2230 | "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; |
| 1084 | 2231 | ||
| 2232 | vector<string> vLockedAuditArgs; | ||
| 2233 | vLockedAuditArgs.push_back("-datadir=" + tmp); | ||
| 2234 | vLockedAuditArgs.push_back("-recoveryaudit"); | ||
| 2235 | vLockedAuditArgs.push_back("-nogui"); | ||
| 2236 | int nLockedAuditRet = RunBitflashChild(strExe, vLockedAuditArgs, NULL, &strLockedAudit); | ||
| 2237 | nFail += Check(nLockedAuditRet == 2 && | ||
| 2238 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit") && | ||
| 2239 | FileContainsText(strLockedAudit, "recovery coverage: unavailable while wallet is locked") && | ||
| 2240 | FileContainsText(strLockedAudit, "Unlock the wallet with /walletpassphrase"), | ||
| 2241 | "a locked encrypted wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2242 | |||
| 1085 | vector<string> vLiteralArgs; | 2243 | vector<string> vLiteralArgs; |
| 1086 | vLiteralArgs.push_back("-datadir=" + tmp); | 2244 | vLiteralArgs.push_back("-datadir=" + tmp); |
| 1087 | vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); | 2245 | vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); |
| @@ -1102,6 +2260,17 @@ static int RunWalletEncryptSelfTest() | |||
| 1102 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | 2260 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), |
| 1103 | "the restarted wallet can decrypt and dump the original key") ? 0 : 1; | 2261 | "the restarted wallet can decrypt and dump the original key") ? 0 : 1; |
| 1104 | 2262 | ||
| 2263 | vector<string> vUnlockedAuditArgs; | ||
| 2264 | vUnlockedAuditArgs.push_back("-datadir=" + tmp); | ||
| 2265 | vUnlockedAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2266 | vUnlockedAuditArgs.push_back("-recoveryaudit"); | ||
| 2267 | vUnlockedAuditArgs.push_back("-nogui"); | ||
| 2268 | int nUnlockedAuditRet = RunBitflashChild(strExe, vUnlockedAuditArgs, NULL, &strUnlockedAudit); | ||
| 2269 | nFail += Check(nUnlockedAuditRet == 0 && | ||
| 2270 | FileContainsText(strUnlockedAudit, "recovery phrase: present") && | ||
| 2271 | !FileContainsText(strUnlockedAudit, "recovery phrase: not installed"), | ||
| 2272 | "an unlocked encrypted wallet audits the installed recovery phrase") ? 0 : 1; | ||
| 2273 | |||
| 1105 | vector<string> vStdinArgs; | 2274 | vector<string> vStdinArgs; |
| 1106 | vStdinArgs.push_back("-datadir=" + tmp); | 2275 | vStdinArgs.push_back("-datadir=" + tmp); |
| 1107 | vStdinArgs.push_back("-walletpassphrase"); | 2276 | vStdinArgs.push_back("-walletpassphrase"); |
| @@ -1132,6 +2301,737 @@ static int RunWalletEncryptSelfTest() | |||
| 1132 | return nFail == 0 ? 0 : 1; | 2301 | return nFail == 0 ? 0 : 1; |
| 1133 | } | 2302 | } |
| 1134 | 2303 | ||
| 2304 | // Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley | ||
| 2305 | // DB encrypt test above, this drives the real command-line path end to end in | ||
| 2306 | // child processes: build a Berkeley DB wallet, export it to SQLite, then run | ||
| 2307 | // -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out | ||
| 2308 | // encrypted, unlockable, and with wallet.dat left untouched. | ||
| 2309 | static int RunWalletSQLiteEncryptSelfTest() | ||
| 2310 | { | ||
| 2311 | fflush(stdout); | ||
| 2312 | printf("wallet-sqlite-encrypt self-test\n"); | ||
| 2313 | |||
| 2314 | std::string tmp; | ||
| 2315 | std::string cwd; | ||
| 2316 | if (!MakeTempDir(tmp)) | ||
| 2317 | { | ||
| 2318 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2319 | return 1; | ||
| 2320 | } | ||
| 2321 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2322 | { | ||
| 2323 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2324 | RemoveTree(tmp); | ||
| 2325 | return 1; | ||
| 2326 | } | ||
| 2327 | |||
| 2328 | int nFail = 0; | ||
| 2329 | strSetDataDir = tmp; | ||
| 2330 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2331 | |||
| 2332 | try | ||
| 2333 | { | ||
| 2334 | // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, | ||
| 2335 | // and a couple of public records, then release it. Everything after this | ||
| 2336 | // runs the real command-line paths in child processes. | ||
| 2337 | if (!LoadWallet()) | ||
| 2338 | throw std::runtime_error("LoadWallet failed"); | ||
| 2339 | |||
| 2340 | string strError; | ||
| 2341 | const string strMnemonic = | ||
| 2342 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; | ||
| 2343 | nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), | ||
| 2344 | "a phrase can be installed before encryption") ? 0 : 1; | ||
| 2345 | TopUpKeyPool(); | ||
| 2346 | |||
| 2347 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2348 | CKey publicLabelKey; | ||
| 2349 | publicLabelKey.MakeNewKey(); | ||
| 2350 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2351 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2352 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2353 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2354 | |||
| 2355 | CPrivKey vchDefaultPrivKey; | ||
| 2356 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2357 | "the default private key is readable before encryption") ? 0 : 1; | ||
| 2358 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); | ||
| 2359 | vector<unsigned char> vchPlainHDMaster = vchHDMaster; | ||
| 2360 | vector<unsigned char> vchPlainHDChainCode = vchHDChainCode; | ||
| 2361 | |||
| 2362 | // Release wallet.dat so a child process can read it for the export. | ||
| 2363 | DBFlush(true); | ||
| 2364 | |||
| 2365 | #ifdef _WIN32 | ||
| 2366 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2367 | #else | ||
| 2368 | string strExe = cwd + "/bitflash-node"; | ||
| 2369 | #endif | ||
| 2370 | string strSQLitePath = tmp + "/wallet.sqlite"; | ||
| 2371 | string strWalletDatPath = tmp + "/wallet.dat"; | ||
| 2372 | string strRightPassFile = tmp + "/right-pass.txt"; | ||
| 2373 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | ||
| 2374 | nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && | ||
| 2375 | WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), | ||
| 2376 | "passphrase files can be written for restarted commands") ? 0 : 1; | ||
| 2377 | |||
| 2378 | // Step 1: export the Berkeley DB wallet to SQLite. | ||
| 2379 | vector<string> vExportArgs; | ||
| 2380 | vExportArgs.push_back("-datadir=" + tmp); | ||
| 2381 | vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); | ||
| 2382 | vExportArgs.push_back("-nogui"); | ||
| 2383 | int nExportRet = RunBitflashChild(strExe, vExportArgs); | ||
| 2384 | nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), | ||
| 2385 | "the wallet exports to a SQLite file") ? 0 : 1; | ||
| 2386 | nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2387 | "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; | ||
| 2388 | |||
| 2389 | // Step 2: encrypt through the real SQLite backend command path. | ||
| 2390 | string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; | ||
| 2391 | vector<string> vEncryptArgs; | ||
| 2392 | vEncryptArgs.push_back("-datadir=" + tmp); | ||
| 2393 | vEncryptArgs.push_back("-walletbackend=sqlite"); | ||
| 2394 | vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); | ||
| 2395 | vEncryptArgs.push_back("-nogui"); | ||
| 2396 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2397 | nFail += Check(nEncryptRet == 0, | ||
| 2398 | "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; | ||
| 2399 | nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), | ||
| 2400 | "encryption took the SQLite persistence path") ? 0 : 1; | ||
| 2401 | nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), | ||
| 2402 | "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; | ||
| 2403 | |||
| 2404 | // The encrypted SQLite wallet must no longer hold the plaintext secrets. | ||
| 2405 | nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2406 | "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; | ||
| 2407 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), | ||
| 2408 | "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; | ||
| 2409 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), | ||
| 2410 | "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; | ||
| 2411 | |||
| 2412 | // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path | ||
| 2413 | // must never touch it, so its plaintext key is still exactly there. | ||
| 2414 | nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), | ||
| 2415 | "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; | ||
| 2416 | |||
| 2417 | // Step 3: a locked encrypted SQLite wallet reports that phrase coverage | ||
| 2418 | // needs an unlock, exactly as the Berkeley DB path does (exit code 2). | ||
| 2419 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2420 | vector<string> vLockedArgs; | ||
| 2421 | vLockedArgs.push_back("-datadir=" + tmp); | ||
| 2422 | vLockedArgs.push_back("-walletbackend=sqlite"); | ||
| 2423 | vLockedArgs.push_back("-recoveryaudit"); | ||
| 2424 | vLockedArgs.push_back("-nogui"); | ||
| 2425 | int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); | ||
| 2426 | nFail += Check(nLockedRet == 2 && | ||
| 2427 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), | ||
| 2428 | "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2429 | |||
| 2430 | // The wrong passphrase is rejected and reveals nothing. | ||
| 2431 | string strWrongDump = tmp + "/wrong-dump.txt"; | ||
| 2432 | vector<string> vWrongArgs; | ||
| 2433 | vWrongArgs.push_back("-datadir=" + tmp); | ||
| 2434 | vWrongArgs.push_back("-walletbackend=sqlite"); | ||
| 2435 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | ||
| 2436 | vWrongArgs.push_back("-dumpwallet=" + strWrongDump); | ||
| 2437 | vWrongArgs.push_back("-nogui"); | ||
| 2438 | int nWrongRet = RunBitflashChild(strExe, vWrongArgs); | ||
| 2439 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | ||
| 2440 | "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; | ||
| 2441 | |||
| 2442 | // The right passphrase unlocks and can dump the original key. | ||
| 2443 | string strRightDump = tmp + "/right-dump.txt"; | ||
| 2444 | vector<string> vRightArgs; | ||
| 2445 | vRightArgs.push_back("-datadir=" + tmp); | ||
| 2446 | vRightArgs.push_back("-walletbackend=sqlite"); | ||
| 2447 | vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2448 | vRightArgs.push_back("-dumpwallet=" + strRightDump); | ||
| 2449 | vRightArgs.push_back("-nogui"); | ||
| 2450 | int nRightRet = RunBitflashChild(strExe, vRightArgs); | ||
| 2451 | nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), | ||
| 2452 | "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; | ||
| 2453 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | ||
| 2454 | "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; | ||
| 2455 | |||
| 2456 | // The recovery phrase survives the encryption. | ||
| 2457 | string strAudit = tmp + "/recovery-audit.txt"; | ||
| 2458 | vector<string> vAuditArgs; | ||
| 2459 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2460 | vAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2461 | vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2462 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2463 | vAuditArgs.push_back("-nogui"); | ||
| 2464 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2465 | nFail += Check(nAuditRet == 0 && | ||
| 2466 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2467 | "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; | ||
| 2468 | |||
| 2469 | // One-shot storage commands honour -walletbackend. The SQLite wallet is | ||
| 2470 | // encrypted; the wallet.dat beside it is still plaintext. Same datadir, | ||
| 2471 | // opposite answers -> the flag routes the diagnostic to the right file. | ||
| 2472 | string strSqliteAuditJson = tmp + "/sqlite-audit.json"; | ||
| 2473 | vector<string> vSqliteAuditArgs; | ||
| 2474 | vSqliteAuditArgs.push_back("-datadir=" + tmp); | ||
| 2475 | vSqliteAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2476 | vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson); | ||
| 2477 | vSqliteAuditArgs.push_back("-nogui"); | ||
| 2478 | int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs); | ||
| 2479 | nFail += Check(nSqliteAuditRet == 0 && | ||
| 2480 | FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") && | ||
| 2481 | FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""), | ||
| 2482 | "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2483 | |||
| 2484 | string strBdbAuditJson = tmp + "/bdb-audit.json"; | ||
| 2485 | vector<string> vBdbAuditArgs; | ||
| 2486 | vBdbAuditArgs.push_back("-datadir=" + tmp); | ||
| 2487 | vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson); | ||
| 2488 | vBdbAuditArgs.push_back("-nogui"); | ||
| 2489 | int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs); | ||
| 2490 | nFail += Check(nBdbAuditRet == 0 && | ||
| 2491 | FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") && | ||
| 2492 | FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""), | ||
| 2493 | "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1; | ||
| 2494 | |||
| 2495 | string strSqliteCheckOut = tmp + "/sqlite-check.txt"; | ||
| 2496 | vector<string> vSqliteCheckArgs; | ||
| 2497 | vSqliteCheckArgs.push_back("-datadir=" + tmp); | ||
| 2498 | vSqliteCheckArgs.push_back("-walletbackend=sqlite"); | ||
| 2499 | vSqliteCheckArgs.push_back("-walletstoragecheck"); | ||
| 2500 | vSqliteCheckArgs.push_back("-nogui"); | ||
| 2501 | int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut); | ||
| 2502 | nFail += Check(nSqliteCheckRet == 0, | ||
| 2503 | "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1; | ||
| 2504 | |||
| 2505 | // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat. | ||
| 2506 | // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds | ||
| 2507 | // none of the plaintext key that wallet.dat still contains. | ||
| 2508 | string strBackupDest = tmp + "/backup.sqlite"; | ||
| 2509 | vector<string> vBackupArgs; | ||
| 2510 | vBackupArgs.push_back("-datadir=" + tmp); | ||
| 2511 | vBackupArgs.push_back("-walletbackend=sqlite"); | ||
| 2512 | vBackupArgs.push_back("-backupwallet=" + strBackupDest); | ||
| 2513 | vBackupArgs.push_back("-nogui"); | ||
| 2514 | int nBackupRet = RunBitflashChild(strExe, vBackupArgs); | ||
| 2515 | nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()), | ||
| 2516 | "-backupwallet under SQLite writes a backup file") ? 0 : 1; | ||
| 2517 | nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes), | ||
| 2518 | "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1; | ||
| 2519 | |||
| 2520 | string strBackupLoadCheck = tmp + "/backup-loadcheck.txt"; | ||
| 2521 | vector<string> vLoadCheckArgs; | ||
| 2522 | vLoadCheckArgs.push_back("-datadir=" + tmp); | ||
| 2523 | vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest); | ||
| 2524 | vLoadCheckArgs.push_back("-nogui"); | ||
| 2525 | int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck); | ||
| 2526 | nFail += Check(nBackupLoadRet == 0, | ||
| 2527 | "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1; | ||
| 2528 | } | ||
| 2529 | catch (const std::exception& e) | ||
| 2530 | { | ||
| 2531 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2532 | nFail++; | ||
| 2533 | } | ||
| 2534 | catch (...) | ||
| 2535 | { | ||
| 2536 | printf(" FAIL unknown exception\n"); | ||
| 2537 | nFail++; | ||
| 2538 | } | ||
| 2539 | |||
| 2540 | DBFlush(true); | ||
| 2541 | SetCurrentDir(cwd); | ||
| 2542 | RemoveTree(tmp); | ||
| 2543 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2544 | nFail, nFail == 1 ? "" : "s"); | ||
| 2545 | fflush(stdout); | ||
| 2546 | return nFail == 0 ? 0 : 1; | ||
| 2547 | } | ||
| 2548 | |||
| 2549 | // Backend resolution when no -walletbackend flag is given: a fresh install | ||
| 2550 | // starts on SQLite (the new default), an existing wallet.dat stays on Berkeley | ||
| 2551 | // DB, the persistent marker overrides the default, and an explicit flag | ||
| 2552 | // overrides everything. Each scenario runs a real child process. | ||
| 2553 | static int RunWalletBackendDefaultSelfTest() | ||
| 2554 | { | ||
| 2555 | fflush(stdout); | ||
| 2556 | printf("wallet-backend-default self-test\n"); | ||
| 2557 | |||
| 2558 | std::string tmp, cwd; | ||
| 2559 | if (!MakeTempDir(tmp)) | ||
| 2560 | { | ||
| 2561 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2562 | return 1; | ||
| 2563 | } | ||
| 2564 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2565 | { | ||
| 2566 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2567 | RemoveTree(tmp); | ||
| 2568 | return 1; | ||
| 2569 | } | ||
| 2570 | |||
| 2571 | int nFail = 0; | ||
| 2572 | strSetDataDir = tmp; | ||
| 2573 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2574 | |||
| 2575 | #ifdef _WIN32 | ||
| 2576 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2577 | #else | ||
| 2578 | string strExe = cwd + "/bitflash-node"; | ||
| 2579 | #endif | ||
| 2580 | |||
| 2581 | try | ||
| 2582 | { | ||
| 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. | ||
| 2588 | string d1 = tmp + "/fresh"; | ||
| 2589 | nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; | ||
| 2590 | vector<string> a1; | ||
| 2591 | a1.push_back("-datadir=" + d1); | ||
| 2592 | a1.push_back("-nomanagedtor"); | ||
| 2593 | a1.push_back("-nogui"); | ||
| 2594 | a1.push_back("-newaddress"); | ||
| 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); | ||
| 2607 | nFail += Check(r1 == 0 && | ||
| 2608 | FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2609 | !FileExists((d1 + "/wallet.dat").c_str()) && | ||
| 2610 | FileContainsText(d1 + "/wallet-backend", "sqlite"), | ||
| 2611 | "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1; | ||
| 2612 | |||
| 2613 | // 1b. Second run reuses it without the flag -- the refusal only guards | ||
| 2614 | // creation, so an existing wallet still starts flagless. | ||
| 2615 | int r1b = RunBitflashChild(strExe, a1); | ||
| 2616 | nFail += Check(r1b == 0 && | ||
| 2617 | FileExists((d1 + "/wallet.sqlite").c_str()) && | ||
| 2618 | !FileExists((d1 + "/wallet.dat").c_str()), | ||
| 2619 | "the recorded SQLite choice survives a flagless restart") ? 0 : 1; | ||
| 2620 | |||
| 2621 | // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. | ||
| 2622 | string d2 = tmp + "/existing-bdb"; | ||
| 2623 | nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1; | ||
| 2624 | vector<string> a2c; | ||
| 2625 | a2c.push_back("-datadir=" + d2); | ||
| 2626 | a2c.push_back("-nomanagedtor"); | ||
| 2627 | a2c.push_back("-nogui"); | ||
| 2628 | a2c.push_back("-walletbackend=bdb"); | ||
| 2629 | a2c.push_back("-newphrase"); | ||
| 2630 | RunBitflashChild(strExe, a2c); | ||
| 2631 | vector<string> a2; | ||
| 2632 | a2.push_back("-datadir=" + d2); | ||
| 2633 | a2.push_back("-nomanagedtor"); | ||
| 2634 | a2.push_back("-nogui"); | ||
| 2635 | a2.push_back("-newaddress"); | ||
| 2636 | int r2 = RunBitflashChild(strExe, a2); | ||
| 2637 | nFail += Check(r2 == 0 && | ||
| 2638 | FileExists((d2 + "/wallet.dat").c_str()) && | ||
| 2639 | !FileExists((d2 + "/wallet.sqlite").c_str()), | ||
| 2640 | "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1; | ||
| 2641 | |||
| 2642 | // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir. | ||
| 2643 | string d3 = tmp + "/marker-bdb"; | ||
| 2644 | nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1; | ||
| 2645 | nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"), | ||
| 2646 | "a bdb marker can be written") ? 0 : 1; | ||
| 2647 | vector<string> a3; | ||
| 2648 | a3.push_back("-datadir=" + d3); | ||
| 2649 | a3.push_back("-nomanagedtor"); | ||
| 2650 | a3.push_back("-nogui"); | ||
| 2651 | a3.push_back("-newaddress"); | ||
| 2652 | int r3 = RunBitflashChild(strExe, a3); | ||
| 2653 | nFail += Check(r3 == 0 && | ||
| 2654 | FileExists((d3 + "/wallet.dat").c_str()) && | ||
| 2655 | !FileExists((d3 + "/wallet.sqlite").c_str()), | ||
| 2656 | "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1; | ||
| 2657 | |||
| 2658 | // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat. | ||
| 2659 | string d4 = tmp + "/explicit-bdb"; | ||
| 2660 | nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1; | ||
| 2661 | vector<string> a4; | ||
| 2662 | a4.push_back("-datadir=" + d4); | ||
| 2663 | a4.push_back("-nomanagedtor"); | ||
| 2664 | a4.push_back("-nogui"); | ||
| 2665 | a4.push_back("-walletbackend=bdb"); | ||
| 2666 | a4.push_back("-newaddress"); | ||
| 2667 | int r4 = RunBitflashChild(strExe, a4); | ||
| 2668 | nFail += Check(r4 == 0 && | ||
| 2669 | FileExists((d4 + "/wallet.dat").c_str()) && | ||
| 2670 | !FileExists((d4 + "/wallet.sqlite").c_str()), | ||
| 2671 | "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1; | ||
| 2672 | } | ||
| 2673 | catch (const std::exception& e) | ||
| 2674 | { | ||
| 2675 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2676 | nFail++; | ||
| 2677 | } | ||
| 2678 | catch (...) | ||
| 2679 | { | ||
| 2680 | printf(" FAIL unknown exception\n"); | ||
| 2681 | nFail++; | ||
| 2682 | } | ||
| 2683 | |||
| 2684 | DBFlush(true); | ||
| 2685 | SetCurrentDir(cwd); | ||
| 2686 | RemoveTree(tmp); | ||
| 2687 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2688 | nFail, nFail == 1 ? "" : "s"); | ||
| 2689 | fflush(stdout); | ||
| 2690 | return nFail == 0 ? 0 : 1; | ||
| 2691 | } | ||
| 2692 | |||
| 2693 | // The wizard's conversion action, headless: build a Berkeley DB wallet, run | ||
| 2694 | // DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a | ||
| 2695 | // usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then | ||
| 2696 | // that a flagless restart opens the converted SQLite wallet. | ||
| 2697 | static int RunWalletConvertSelfTest() | ||
| 2698 | { | ||
| 2699 | fflush(stdout); | ||
| 2700 | printf("wallet-convert self-test\n"); | ||
| 2701 | |||
| 2702 | std::string tmp, cwd; | ||
| 2703 | if (!MakeTempDir(tmp)) | ||
| 2704 | { | ||
| 2705 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2706 | return 1; | ||
| 2707 | } | ||
| 2708 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2709 | { | ||
| 2710 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2711 | RemoveTree(tmp); | ||
| 2712 | return 1; | ||
| 2713 | } | ||
| 2714 | |||
| 2715 | int nFail = 0; | ||
| 2716 | strSetDataDir = tmp; | ||
| 2717 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2718 | |||
| 2719 | #ifdef _WIN32 | ||
| 2720 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2721 | #else | ||
| 2722 | string strExe = cwd + "/bitflash-node"; | ||
| 2723 | #endif | ||
| 2724 | |||
| 2725 | try | ||
| 2726 | { | ||
| 2727 | if (!LoadWallet()) | ||
| 2728 | throw std::runtime_error("LoadWallet failed"); | ||
| 2729 | |||
| 2730 | string strError; | ||
| 2731 | nFail += Check(SetHDSeedFromMnemonic( | ||
| 2732 | "abandon abandon abandon abandon abandon abandon abandon " | ||
| 2733 | "abandon abandon abandon abandon about", strError), | ||
| 2734 | "a phrase can be installed before conversion") ? 0 : 1; | ||
| 2735 | TopUpKeyPool(); | ||
| 2736 | |||
| 2737 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2738 | CPrivKey vchDefaultPrivKey; | ||
| 2739 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2740 | "the default private key is readable before conversion") ? 0 : 1; | ||
| 2741 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), | ||
| 2742 | vchDefaultPrivKey.end()); | ||
| 2743 | |||
| 2744 | // The conversion action the GUI button invokes. | ||
| 2745 | int nCopied = 0; | ||
| 2746 | string strConvertError; | ||
| 2747 | bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); | ||
| 2748 | nFail += Check(fConverted && nCopied > 0, | ||
| 2749 | "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; | ||
| 2750 | nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && | ||
| 2751 | FileContainsText(tmp + "/wallet-backend", "sqlite"), | ||
| 2752 | "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; | ||
| 2753 | nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), | ||
| 2754 | "wallet.dat is left untouched by the conversion") ? 0 : 1; | ||
| 2755 | |||
| 2756 | // Idempotence guard: a second conversion refuses rather than overwrite. | ||
| 2757 | int nCopied2 = 0; | ||
| 2758 | string strSecondError; | ||
| 2759 | nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), | ||
| 2760 | "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; | ||
| 2761 | |||
| 2762 | DBFlush(true); | ||
| 2763 | |||
| 2764 | // A flagless restart now opens the converted SQLite wallet (the marker | ||
| 2765 | // routes it) and finds the same recovery phrase. | ||
| 2766 | string strAudit = tmp + "/converted-recovery-audit.txt"; | ||
| 2767 | vector<string> vAuditArgs; | ||
| 2768 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2769 | vAuditArgs.push_back("-nomanagedtor"); | ||
| 2770 | vAuditArgs.push_back("-nogui"); | ||
| 2771 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2772 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2773 | nFail += Check(nAuditRet == 0 && | ||
| 2774 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2775 | "a flagless restart opens the converted SQLite wallet") ? 0 : 1; | ||
| 2776 | } | ||
| 2777 | catch (const std::exception& e) | ||
| 2778 | { | ||
| 2779 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2780 | nFail++; | ||
| 2781 | } | ||
| 2782 | catch (...) | ||
| 2783 | { | ||
| 2784 | printf(" FAIL unknown exception\n"); | ||
| 2785 | nFail++; | ||
| 2786 | } | ||
| 2787 | |||
| 2788 | DBFlush(true); | ||
| 2789 | SetCurrentDir(cwd); | ||
| 2790 | RemoveTree(tmp); | ||
| 2791 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2792 | nFail, nFail == 1 ? "" : "s"); | ||
| 2793 | fflush(stdout); | ||
| 2794 | return nFail == 0 ? 0 : 1; | ||
| 2795 | } | ||
| 2796 | |||
| 2797 | static int RunWalletPortabilitySelfTest() | ||
| 2798 | { | ||
| 2799 | fflush(stdout); | ||
| 2800 | printf("wallet-portability self-test\n"); | ||
| 2801 | |||
| 2802 | std::string tmp; | ||
| 2803 | std::string cwd; | ||
| 2804 | if (!MakeTempDir(tmp)) | ||
| 2805 | { | ||
| 2806 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2807 | return 1; | ||
| 2808 | } | ||
| 2809 | if (!GetCurrentDir(cwd)) | ||
| 2810 | { | ||
| 2811 | printf(" FAIL could not read current directory\n"); | ||
| 2812 | RemoveTree(tmp); | ||
| 2813 | return 1; | ||
| 2814 | } | ||
| 2815 | |||
| 2816 | int nFail = 0; | ||
| 2817 | printf(" temp root: %s\n", tmp.c_str()); | ||
| 2818 | |||
| 2819 | try | ||
| 2820 | { | ||
| 2821 | #ifdef _WIN32 | ||
| 2822 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2823 | #else | ||
| 2824 | string strExe = cwd + "/bitflash-node"; | ||
| 2825 | #endif | ||
| 2826 | string strOriginal = tmp + "/original"; | ||
| 2827 | string strRawCopy = tmp + "/raw-copy"; | ||
| 2828 | string strBackupCopy = tmp + "/backup-copy"; | ||
| 2829 | nFail += Check(MakeDirLocal(strOriginal) && | ||
| 2830 | MakeDirLocal(strRawCopy) && | ||
| 2831 | MakeDirLocal(strBackupCopy), | ||
| 2832 | "temporary wallet directories can be created") ? 0 : 1; | ||
| 2833 | |||
| 2834 | string strCreateOut = tmp + "/create.txt"; | ||
| 2835 | vector<string> vCreateArgs; | ||
| 2836 | vCreateArgs.push_back("-datadir=" + strOriginal); | ||
| 2837 | vCreateArgs.push_back("-nomanagedtor"); | ||
| 2838 | vCreateArgs.push_back("-nogui"); | ||
| 2839 | vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability | ||
| 2840 | vCreateArgs.push_back("-newphrase"); | ||
| 2841 | int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); | ||
| 2842 | nFail += Check(nCreateRet == 0 && | ||
| 2843 | FileContainsText(strCreateOut, "Write these twelve words down"), | ||
| 2844 | "a phrase wallet can be created in a fresh datadir") ? 0 : 1; | ||
| 2845 | |||
| 2846 | string strOriginalAudit = tmp + "/original-audit.txt"; | ||
| 2847 | vector<string> vOriginalAuditArgs; | ||
| 2848 | vOriginalAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2849 | vOriginalAuditArgs.push_back("-nomanagedtor"); | ||
| 2850 | vOriginalAuditArgs.push_back("-nogui"); | ||
| 2851 | vOriginalAuditArgs.push_back("-recoveryaudit"); | ||
| 2852 | int nOriginalAuditRet = RunBitflashChild(strExe, vOriginalAuditArgs, NULL, &strOriginalAudit); | ||
| 2853 | nFail += Check(nOriginalAuditRet == 0 && | ||
| 2854 | FileContainsText(strOriginalAudit, "recovery phrase: present") && | ||
| 2855 | FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"), | ||
| 2856 | "the original wallet reports its recovery phrase") ? 0 : 1; | ||
| 2857 | |||
| 2858 | string strOriginalStorageAudit = tmp + "/original-storage-audit.txt"; | ||
| 2859 | vector<string> vOriginalStorageAuditArgs; | ||
| 2860 | vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2861 | vOriginalStorageAuditArgs.push_back("-nomanagedtor"); | ||
| 2862 | vOriginalStorageAuditArgs.push_back("-nogui"); | ||
| 2863 | vOriginalStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2864 | int nOriginalStorageAuditRet = | ||
| 2865 | RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit); | ||
| 2866 | nFail += Check(nOriginalStorageAuditRet == 0 && | ||
| 2867 | FileContainsText(strOriginalStorageAudit, "Wallet storage audit") && | ||
| 2868 | FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") && | ||
| 2869 | FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") && | ||
| 2870 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), | ||
| 2871 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; | ||
| 2872 | |||
| 2873 | string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; | ||
| 2874 | vector<string> vOriginalStorageCheckArgs; | ||
| 2875 | vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 2876 | vOriginalStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 2877 | vOriginalStorageCheckArgs.push_back("-nogui"); | ||
| 2878 | vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 2879 | int nOriginalStorageCheckRet = | ||
| 2880 | RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); | ||
| 2881 | nFail += Check(nOriginalStorageCheckRet == 0 && | ||
| 2882 | FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && | ||
| 2883 | FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), | ||
| 2884 | "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; | ||
| 2885 | |||
| 2886 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; | ||
| 2887 | vector<string> vOriginalStorageAuditJsonArgs; | ||
| 2888 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | ||
| 2889 | vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor"); | ||
| 2890 | vOriginalStorageAuditJsonArgs.push_back("-nogui"); | ||
| 2891 | vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + | ||
| 2892 | strOriginalStorageAuditJson); | ||
| 2893 | int nOriginalStorageAuditJsonRet = | ||
| 2894 | RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs); | ||
| 2895 | nFail += Check(nOriginalStorageAuditJsonRet == 0 && | ||
| 2896 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2897 | "\"format\": \"bitflash-wallet-storage-audit-v1\"") && | ||
| 2898 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2899 | "\"plain_hd_seed\": \"complete\"") && | ||
| 2900 | FileContainsText(strOriginalStorageAuditJson, | ||
| 2901 | "\"encrypted_hd_seed\": \"none\""), | ||
| 2902 | "the storage audit can write plaintext-wallet JSON") ? 0 : 1; | ||
| 2903 | |||
| 2904 | string strPortableWallet = tmp + "/portable-wallet.dat"; | ||
| 2905 | string strBackupOut = tmp + "/backupwallet.txt"; | ||
| 2906 | vector<string> vBackupArgs; | ||
| 2907 | vBackupArgs.push_back("-datadir=" + strOriginal); | ||
| 2908 | vBackupArgs.push_back("-nomanagedtor"); | ||
| 2909 | vBackupArgs.push_back("-nogui"); | ||
| 2910 | vBackupArgs.push_back("-backupwallet=" + strPortableWallet); | ||
| 2911 | int nBackupRet = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut); | ||
| 2912 | nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()), | ||
| 2913 | "/backupwallet writes a portable wallet.dat") ? 0 : 1; | ||
| 2914 | |||
| 2915 | string strBackupOut2 = tmp + "/backupwallet-overwrite.txt"; | ||
| 2916 | int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2); | ||
| 2917 | nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) && | ||
| 2918 | !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") && | ||
| 2919 | !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."), | ||
| 2920 | "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1; | ||
| 2921 | |||
| 2922 | nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat", | ||
| 2923 | strRawCopy + "/wallet.dat"), | ||
| 2924 | "a raw wallet.dat can be copied without database logs") ? 0 : 1; | ||
| 2925 | string strRawAudit = tmp + "/raw-copy-audit.txt"; | ||
| 2926 | vector<string> vRawAuditArgs; | ||
| 2927 | vRawAuditArgs.push_back("-datadir=" + strRawCopy); | ||
| 2928 | vRawAuditArgs.push_back("-nomanagedtor"); | ||
| 2929 | vRawAuditArgs.push_back("-nogui"); | ||
| 2930 | vRawAuditArgs.push_back("-recoveryaudit"); | ||
| 2931 | int nRawAuditRet = RunBitflashChild(strExe, vRawAuditArgs, NULL, &strRawAudit); | ||
| 2932 | nFail += Check(nRawAuditRet == 0 && | ||
| 2933 | FileContainsText(strRawAudit, "recovery phrase: present") && | ||
| 2934 | !FileContainsText(strRawAudit, "Cannot open wallet.dat"), | ||
| 2935 | "a raw wallet.dat copy opens in a new datadir") ? 0 : 1; | ||
| 2936 | |||
| 2937 | nFail += Check(CopyFileLocal(strPortableWallet, | ||
| 2938 | strBackupCopy + "/wallet.dat"), | ||
| 2939 | "the portable backup can be installed as wallet.dat") ? 0 : 1; | ||
| 2940 | string strBackupAudit = tmp + "/backup-copy-audit.txt"; | ||
| 2941 | vector<string> vBackupAuditArgs; | ||
| 2942 | vBackupAuditArgs.push_back("-datadir=" + strBackupCopy); | ||
| 2943 | vBackupAuditArgs.push_back("-nomanagedtor"); | ||
| 2944 | vBackupAuditArgs.push_back("-nogui"); | ||
| 2945 | vBackupAuditArgs.push_back("-recoveryaudit"); | ||
| 2946 | int nBackupAuditRet = RunBitflashChild(strExe, vBackupAuditArgs, NULL, &strBackupAudit); | ||
| 2947 | nFail += Check(nBackupAuditRet == 0 && | ||
| 2948 | FileContainsText(strBackupAudit, "recovery phrase: present") && | ||
| 2949 | FileContainsText(strBackupAudit, "SLIP-0044 BITFLASH"), | ||
| 2950 | "a /backupwallet copy opens in a new datadir") ? 0 : 1; | ||
| 2951 | |||
| 2952 | string strPassFile = tmp + "/encrypt-pass.txt"; | ||
| 2953 | string strEncryptOut = tmp + "/encryptwallet.txt"; | ||
| 2954 | nFail += Check(WriteTextFile(strPassFile, "portable-test-passphrase\n"), | ||
| 2955 | "an encryption passphrase file can be written") ? 0 : 1; | ||
| 2956 | vector<string> vEncryptArgs; | ||
| 2957 | vEncryptArgs.push_back("-datadir=" + strOriginal); | ||
| 2958 | vEncryptArgs.push_back("-nomanagedtor"); | ||
| 2959 | vEncryptArgs.push_back("-nogui"); | ||
| 2960 | vEncryptArgs.push_back("-encryptwallet=@" + strPassFile); | ||
| 2961 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2962 | nFail += Check(nEncryptRet == 0 && | ||
| 2963 | !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."), | ||
| 2964 | "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1; | ||
| 2965 | |||
| 2966 | string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt"; | ||
| 2967 | vector<string> vEncryptedStorageAuditArgs; | ||
| 2968 | vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal); | ||
| 2969 | vEncryptedStorageAuditArgs.push_back("-nomanagedtor"); | ||
| 2970 | vEncryptedStorageAuditArgs.push_back("-nogui"); | ||
| 2971 | vEncryptedStorageAuditArgs.push_back("-walletstorageaudit"); | ||
| 2972 | int nEncryptedStorageAuditRet = | ||
| 2973 | RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit); | ||
| 2974 | nFail += Check(nEncryptedStorageAuditRet == 0 && | ||
| 2975 | FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") && | ||
| 2976 | FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") && | ||
| 2977 | FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") && | ||
| 2978 | FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") && | ||
| 2979 | FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") && | ||
| 2980 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), | ||
| 2981 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; | ||
| 2982 | |||
| 2983 | string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; | ||
| 2984 | vector<string> vEncryptedStorageCheckArgs; | ||
| 2985 | vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 2986 | vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 2987 | vEncryptedStorageCheckArgs.push_back("-nogui"); | ||
| 2988 | vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 2989 | int nEncryptedStorageCheckRet = | ||
| 2990 | RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); | ||
| 2991 | nFail += Check(nEncryptedStorageCheckRet == 0 && | ||
| 2992 | FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && | ||
| 2993 | FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && | ||
| 2994 | FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), | ||
| 2995 | "the storage sanity check accepts an encrypted wallet") ? 0 : 1; | ||
| 2996 | |||
| 2997 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; | ||
| 2998 | vector<string> vEncryptedStorageAuditJsonArgs; | ||
| 2999 | vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | ||
| 3000 | vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor"); | ||
| 3001 | vEncryptedStorageAuditJsonArgs.push_back("-nogui"); | ||
| 3002 | vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + | ||
| 3003 | strEncryptedStorageAuditJson); | ||
| 3004 | int nEncryptedStorageAuditJsonRet = | ||
| 3005 | RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs); | ||
| 3006 | nFail += Check(nEncryptedStorageAuditJsonRet == 0 && | ||
| 3007 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3008 | "\"plain_private_keys\": 0") && | ||
| 3009 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3010 | "\"plain_hd_seed\": \"none\"") && | ||
| 3011 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3012 | "\"encrypted_hd_seed\": \"complete\"") && | ||
| 3013 | FileContainsText(strEncryptedStorageAuditJson, | ||
| 3014 | "\"wallet_minimum_version\": \"present\""), | ||
| 3015 | "the storage audit can write encrypted-wallet JSON") ? 0 : 1; | ||
| 3016 | } | ||
| 3017 | catch (const std::exception& e) | ||
| 3018 | { | ||
| 3019 | printf(" FAIL exception: %s\n", e.what()); | ||
| 3020 | nFail++; | ||
| 3021 | } | ||
| 3022 | catch (...) | ||
| 3023 | { | ||
| 3024 | printf(" FAIL unknown exception\n"); | ||
| 3025 | nFail++; | ||
| 3026 | } | ||
| 3027 | |||
| 3028 | RemoveTree(tmp); | ||
| 3029 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 3030 | nFail, nFail == 1 ? "" : "s"); | ||
| 3031 | fflush(stdout); | ||
| 3032 | return nFail == 0 ? 0 : 1; | ||
| 3033 | } | ||
| 3034 | |||
| 1135 | static int RunNetMessageSelfTest() | 3035 | static int RunNetMessageSelfTest() |
| 1136 | { | 3036 | { |
| 1137 | fflush(stdout); | 3037 | fflush(stdout); |
| @@ -1235,6 +3135,74 @@ static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) | |||
| 1235 | return block; | 3135 | return block; |
| 1236 | } | 3136 | } |
| 1237 | 3137 | ||
| 3138 | // A distinct transaction of a chosen weight and verification cost. nUnique | ||
| 3139 | // only keeps the txids apart, so a mempool built from these is not a pile of | ||
| 3140 | // duplicates a block could never carry anyway. | ||
| 3141 | static CTransaction MakeSizedTx(size_t nPadBytes, unsigned int nSigOps, int nUnique) | ||
| 3142 | { | ||
| 3143 | CTransaction tx; | ||
| 3144 | tx.vin.push_back(CTxIn(COutPoint(), CScript() << (int64)nUnique << 1)); | ||
| 3145 | tx.vout.push_back(CTxOut(0, CScript())); | ||
| 3146 | for (unsigned int i = 0; i < nSigOps; i++) | ||
| 3147 | tx.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 3148 | tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nPadBytes, 0); | ||
| 3149 | return tx; | ||
| 3150 | } | ||
| 3151 | |||
| 3152 | static CBlock MakeCoinbaseOnlyTemplate() | ||
| 3153 | { | ||
| 3154 | CTransaction txNew; | ||
| 3155 | txNew.vin.resize(1); | ||
| 3156 | txNew.vin[0].prevout.SetNull(); | ||
| 3157 | txNew.vin[0].scriptSig << 1 << 1 << 1; | ||
| 3158 | txNew.vout.resize(1); | ||
| 3159 | txNew.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 3160 | |||
| 3161 | CBlock block; | ||
| 3162 | block.vtx.push_back(txNew); | ||
| 3163 | block.nVersion = 1; | ||
| 3164 | block.nTime = GetAdjustedTime(); | ||
| 3165 | block.nBits = bnProofOfWorkLimit.GetCompact(); | ||
| 3166 | block.nNonce = 0; | ||
| 3167 | return block; | ||
| 3168 | } | ||
| 3169 | |||
| 3170 | // The sum CheckBlock() takes, spelled the same way. | ||
| 3171 | static unsigned int BlockSigOpCount(const CBlock& block) | ||
| 3172 | { | ||
| 3173 | unsigned int n = 0; | ||
| 3174 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 3175 | n += block.vtx[i].GetSigOpCount(); | ||
| 3176 | return n; | ||
| 3177 | } | ||
| 3178 | |||
| 3179 | // Drive the selection rule the way both assemblers drive it: ask | ||
| 3180 | // BlockTemplateFits before appending, skip what does not fit, never stop early | ||
| 3181 | // on one that does not. | ||
| 3182 | // | ||
| 3183 | // This mirrors the loops in BitcoinMiner() and RebuildJob() rather than calling | ||
| 3184 | // them -- one needs a CTxDB, a wallet key and cs_main, the other is static in | ||
| 3185 | // rpc.cpp, so neither is reachable from here. What that leaves proven is the | ||
| 3186 | // rule the two of them share and the property it is meant to guarantee, which | ||
| 3187 | // is where the defect actually lived: the budget was right and the enforcement | ||
| 3188 | // was not. | ||
| 3189 | static void SelectIntoTemplate(CBlock& block, const vector<CTransaction>& vMempool) | ||
| 3190 | { | ||
| 3191 | unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK); | ||
| 3192 | unsigned int nBlockSigOps = block.vtx[0].GetSigOpCount(); | ||
| 3193 | for (int i = 0; i < (int)vMempool.size(); i++) | ||
| 3194 | { | ||
| 3195 | const CTransaction& tx = vMempool[i]; | ||
| 3196 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 3197 | unsigned int nTxSigOps = tx.GetSigOpCount(); | ||
| 3198 | if (!BlockTemplateFits(nBlockSize, nBlockSigOps, nTxSize, nTxSigOps)) | ||
| 3199 | continue; | ||
| 3200 | block.vtx.push_back(tx); | ||
| 3201 | nBlockSize += nTxSize; | ||
| 3202 | nBlockSigOps += nTxSigOps; | ||
| 3203 | } | ||
| 3204 | } | ||
| 3205 | |||
| 1238 | static int RunConsensusLimitsSelfTest() | 3206 | static int RunConsensusLimitsSelfTest() |
| 1239 | { | 3207 | { |
| 1240 | fflush(stdout); | 3208 | fflush(stdout); |
| @@ -1258,6 +3226,95 @@ static int RunConsensusLimitsSelfTest() | |||
| 1258 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; | 3226 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; |
| 1259 | nFail += Check(!oversized.CheckSizeLimits(), | 3227 | nFail += Check(!oversized.CheckSizeLimits(), |
| 1260 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; | 3228 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; |
| 3229 | |||
| 3230 | // The other half of the cap. Everything above asks what validation | ||
| 3231 | // refuses; nothing asked what assembly produces, which is why a miner | ||
| 3232 | // that could overshoot the cap by an entire mempool went unnoticed. | ||
| 3233 | nFail += Check(MAX_BLOCK_SIZE_GEN < MAX_BLOCK_SIZE, | ||
| 3234 | "the assembly budget stays under the consensus cap") ? 0 : 1; | ||
| 3235 | |||
| 3236 | // The rule itself, at its edges. The sigop comparison has to match | ||
| 3237 | // CheckBlock(), which fails on `> MAX_BLOCK_SIGOPS` and so accepts a | ||
| 3238 | // block sitting exactly on the limit. | ||
| 3239 | nFail += Check(BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 100, 0), | ||
| 3240 | "a transaction that exactly fills the size budget fits") ? 0 : 1; | ||
| 3241 | nFail += Check(!BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 101, 0), | ||
| 3242 | "one byte past the size budget does not fit") ? 0 : 1; | ||
| 3243 | nFail += Check(BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS), | ||
| 3244 | "sigops exactly on the limit fit, as CheckBlock() allows") ? 0 : 1; | ||
| 3245 | nFail += Check(!BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS + 1), | ||
| 3246 | "one sigop past the limit does not fit") ? 0 : 1; | ||
| 3247 | |||
| 3248 | // A mempool several times the consensus cap, in ordinary-sized | ||
| 3249 | // transactions. The old miner tested its budget only on the outer | ||
| 3250 | // loop, so a single pass appended all of this. | ||
| 3251 | { | ||
| 3252 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3253 | vector<CTransaction> vMempool; | ||
| 3254 | for (int i = 0; i < 300; i++) | ||
| 3255 | vMempool.push_back(MakeSizedTx(10000, 1, i)); | ||
| 3256 | SelectIntoTemplate(block, vMempool); | ||
| 3257 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3258 | |||
| 3259 | nFail += Check(block.vtx.size() > 1, | ||
| 3260 | "a full mempool still fills a template") ? 0 : 1; | ||
| 3261 | nFail += Check(::GetSerializeSize(block, SER_DISK) <= MAX_BLOCK_SIZE_GEN, | ||
| 3262 | "a template built from a 3 MB mempool respects the budget") ? 0 : 1; | ||
| 3263 | nFail += Check(block.CheckSizeLimits(), | ||
| 3264 | "that template is one CheckBlock() would accept on size") ? 0 : 1; | ||
| 3265 | } | ||
| 3266 | |||
| 3267 | // One transaction too big for the budget. Nothing on the path into the | ||
| 3268 | // mempool caps transaction size, so this is a transaction a peer can | ||
| 3269 | // simply send. It must be skipped and the rest still taken -- skipped, | ||
| 3270 | // not stopped on. | ||
| 3271 | { | ||
| 3272 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3273 | vector<CTransaction> vMempool; | ||
| 3274 | vMempool.push_back(MakeSizedTx(1500000, 1, 9999)); | ||
| 3275 | for (int i = 0; i < 10; i++) | ||
| 3276 | vMempool.push_back(MakeSizedTx(1000, 1, i)); | ||
| 3277 | |||
| 3278 | unsigned int nHugeSize = ::GetSerializeSize(vMempool[0], SER_NETWORK); | ||
| 3279 | nFail += Check(nHugeSize > MAX_BLOCK_SIZE && nHugeSize <= MAX_SIZE, | ||
| 3280 | "the outsized transaction is one the mempool would hold") ? 0 : 1; | ||
| 3281 | |||
| 3282 | SelectIntoTemplate(block, vMempool); | ||
| 3283 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3284 | |||
| 3285 | bool fTookHuge = false; | ||
| 3286 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 3287 | if (block.vtx[i].GetHash() == vMempool[0].GetHash()) | ||
| 3288 | fTookHuge = true; | ||
| 3289 | |||
| 3290 | nFail += Check(!fTookHuge, | ||
| 3291 | "a transaction larger than the budget is left out") ? 0 : 1; | ||
| 3292 | nFail += Check(block.vtx.size() == 11, | ||
| 3293 | "the ten that do fit are taken anyway") ? 0 : 1; | ||
| 3294 | nFail += Check(block.CheckSizeLimits(), | ||
| 3295 | "one outsized transaction cannot invalidate a template") ? 0 : 1; | ||
| 3296 | } | ||
| 3297 | |||
| 3298 | // Transactions that are cheap in bytes and expensive to verify. Size | ||
| 3299 | // alone does not bound what a block costs, and CheckBlock() refuses on | ||
| 3300 | // the count regardless of how small the block is. | ||
| 3301 | { | ||
| 3302 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 3303 | vector<CTransaction> vMempool; | ||
| 3304 | for (int i = 0; i < 100; i++) | ||
| 3305 | vMempool.push_back(MakeSizedTx(0, 1000, i)); | ||
| 3306 | SelectIntoTemplate(block, vMempool); | ||
| 3307 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 3308 | |||
| 3309 | nFail += Check(block.vtx.size() > 1, | ||
| 3310 | "sigop-heavy transactions are still selected") ? 0 : 1; | ||
| 3311 | nFail += Check(BlockSigOpCount(block) <= MAX_BLOCK_SIGOPS, | ||
| 3312 | "selection stops at the sigop cap CheckBlock() enforces") ? 0 : 1; | ||
| 3313 | nFail += Check(::GetSerializeSize(block, SER_DISK) < MAX_BLOCK_SIZE_GEN, | ||
| 3314 | "and stops on sigops while still well inside the size budget") ? 0 : 1; | ||
| 3315 | nFail += Check(block.CheckSizeLimits(), | ||
| 3316 | "the sigop-bounded template is within the size limit too") ? 0 : 1; | ||
| 3317 | } | ||
| 1261 | } | 3318 | } |
| 1262 | catch (const std::exception& e) | 3319 | catch (const std::exception& e) |
| 1263 | { | 3320 | { |
| @@ -1637,10 +3694,26 @@ int RunSelfTest(const std::string& name) | |||
| 1637 | return RunWalletHDSelfTest(); | 3694 | return RunWalletHDSelfTest(); |
| 1638 | if (name == "wallet-format") | 3695 | if (name == "wallet-format") |
| 1639 | return RunWalletFormatSelfTest(); | 3696 | return RunWalletFormatSelfTest(); |
| 3697 | if (name == "wallet-storage-sanity") | ||
| 3698 | return RunWalletStorageSanitySelfTest(); | ||
| 3699 | if (name == "db-env-reopen") | ||
| 3700 | return RunDbEnvReopenSelfTest(); | ||
| 3701 | if (name == "wallet-sqlite") | ||
| 3702 | return RunWalletSQLiteSelfTest(); | ||
| 3703 | if (name == "wallet-sqlite-migration") | ||
| 3704 | return RunWalletSQLiteMigrationSelfTest(); | ||
| 1640 | if (name == "wallet-crypto") | 3705 | if (name == "wallet-crypto") |
| 1641 | return RunWalletCryptoSelfTest(); | 3706 | return RunWalletCryptoSelfTest(); |
| 1642 | if (name == "wallet-encrypt") | 3707 | if (name == "wallet-encrypt") |
| 1643 | return RunWalletEncryptSelfTest(); | 3708 | return RunWalletEncryptSelfTest(); |
| 3709 | if (name == "wallet-sqlite-encrypt") | ||
| 3710 | return RunWalletSQLiteEncryptSelfTest(); | ||
| 3711 | if (name == "wallet-backend-default") | ||
| 3712 | return RunWalletBackendDefaultSelfTest(); | ||
| 3713 | if (name == "wallet-convert") | ||
| 3714 | return RunWalletConvertSelfTest(); | ||
| 3715 | if (name == "wallet-portability") | ||
| 3716 | return RunWalletPortabilitySelfTest(); | ||
| 1644 | if (name == "net-message") | 3717 | if (name == "net-message") |
| 1645 | return RunNetMessageSelfTest(); | 3718 | return RunNetMessageSelfTest(); |
| 1646 | if (name == "consensus-limits") | 3719 | if (name == "consensus-limits") |
| @@ -1657,6 +3730,6 @@ int RunSelfTest(const std::string& name) | |||
| 1657 | return RunManagedTorSelfTest(); | 3730 | return RunManagedTorSelfTest(); |
| 1658 | 3731 | ||
| 1659 | printf("Unknown self-test '%s'\n", name.c_str()); | 3732 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 1660 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n"); | 3733 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n"); |
| 1661 | return 1; | 3734 | return 1; |
| 1662 | } | 3735 | } |
diff --git a/src/selftest.h b/src/selftest.h index d3699c6..0ffe232 100644 --- a/src/selftest.h +++ b/src/selftest.h | |||
| @@ -7,5 +7,6 @@ | |||
| 7 | #include <string> | 7 | #include <string> |
| 8 | 8 | ||
| 9 | int RunSelfTest(const std::string& name); | 9 | int RunSelfTest(const std::string& name); |
| 10 | int RunSelfTestMutateWallet(const std::string& name); | ||
| 10 | 11 | ||
| 11 | #endif | 12 | #endif |
diff --git a/src/util.cpp b/src/util.cpp index 0cd0133..ab29178 100644 --- a/src/util.cpp +++ b/src/util.cpp | |||
| @@ -7,6 +7,12 @@ | |||
| 7 | 7 | ||
| 8 | 8 | ||
| 9 | bool fDebug = false; | 9 | bool fDebug = false; |
| 10 | bool fDebugRelay = false; | ||
| 11 | |||
| 12 | // In-process cap for debug.log, in bytes. Default matches the 1G trigger of | ||
| 13 | // the shipped logrotate policy so a long-running node stays bounded even | ||
| 14 | // without it; -maxdebugfilesize=N (MB) tunes it, 0 disables. | ||
| 15 | int64 nMaxDebugFileSize = 1024 * 1024 * (int64)1024; | ||
| 10 | 16 | ||
| 11 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
| 12 | // Categorized, burst-collapsing logging -- see the LogPrint doc comment in | 18 | // Categorized, burst-collapsing logging -- see the LogPrint doc comment in |
| @@ -26,10 +32,22 @@ static bool LogCategoryDefaultOn(const char* category) | |||
| 26 | strcmp(category, "payout") == 0; | 32 | strcmp(category, "payout") == 0; |
| 27 | } | 33 | } |
| 28 | 34 | ||
| 35 | // Categories so noisy they drown out everything else even under -debug: | ||
| 36 | // per-message send traces, wallet relay rebroadcasts, raw CBlock dumps and | ||
| 37 | // per-address discovery lines made up ~90% of a 13-hour production capture | ||
| 38 | // (issue #35). They need /debugrelay, not merely /debug. | ||
| 39 | static bool LogCategoryVerboseOnly(const char* category) | ||
| 40 | { | ||
| 41 | return strcmp(category, "relay") == 0 || | ||
| 42 | strcmp(category, "nostrdiscovery") == 0; | ||
| 43 | } | ||
| 44 | |||
| 29 | bool LogAcceptsCategory(const char* category) | 45 | bool LogAcceptsCategory(const char* category) |
| 30 | { | 46 | { |
| 31 | if (LogCategoryDefaultOn(category)) | 47 | if (LogCategoryDefaultOn(category)) |
| 32 | return true; | 48 | return true; |
| 49 | if (LogCategoryVerboseOnly(category)) | ||
| 50 | return fDebugRelay; | ||
| 33 | return fDebug; | 51 | return fDebug; |
| 34 | } | 52 | } |
| 35 | 53 | ||
| @@ -47,6 +65,61 @@ struct LogDedupState { | |||
| 47 | static std::map<std::string, LogDedupState> g_logDedup; | 65 | static std::map<std::string, LogDedupState> g_logDedup; |
| 48 | static CCriticalSection cs_logDedup; | 66 | static CCriticalSection cs_logDedup; |
| 49 | 67 | ||
| 68 | // A pending "(N similar)" summary, lifted out of the table while the lock is | ||
| 69 | // held and written after it is released. | ||
| 70 | struct LogDedupFlush { | ||
| 71 | std::string category; | ||
| 72 | int suppressed; | ||
| 73 | int64 elapsed; // how long the window that counted them ran | ||
| 74 | std::string lastMsg; | ||
| 75 | }; | ||
| 76 | |||
| 77 | static int64 g_logDedupLastSweep = 0; | ||
| 78 | |||
| 79 | static void EmitDedupSummary(const LogDedupFlush& f) | ||
| 80 | { | ||
| 81 | OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", | ||
| 82 | f.category.c_str(), f.suppressed, | ||
| 83 | (long long)f.elapsed, f.lastMsg.c_str()); | ||
| 84 | } | ||
| 85 | |||
| 86 | // A window is otherwise only closed out when its own call site fires again, so | ||
| 87 | // a burst that simply stops -- 100 ledger entries voided in one pass and then | ||
| 88 | // silence -- leaves its count sitting in the table and the summary is never | ||
| 89 | // written at all. That is how 99 of 100 voids went unreported, with no "(N | ||
| 90 | // similar)" line to say anything was missing (issue #3). Sweep every expired | ||
| 91 | // window instead, so a finished burst is summarised as soon as anything at all | ||
| 92 | // is logged. | ||
| 93 | // | ||
| 94 | // Expired entries are erased rather than reopened: the window is over, so there | ||
| 95 | // is no state left worth keeping, and the next call from that site then takes | ||
| 96 | // the "first time" path and prints directly -- exactly what the rollover branch | ||
| 97 | // below would have done. It also stops the table growing without bound. | ||
| 98 | // | ||
| 99 | // Caller holds cs_logDedup; the summaries are written after it is released. | ||
| 100 | static void SweepExpiredWindowsLocked(int64 now, std::vector<LogDedupFlush>& out) | ||
| 101 | { | ||
| 102 | for (std::map<std::string, LogDedupState>::iterator it = g_logDedup.begin(); | ||
| 103 | it != g_logDedup.end(); ) | ||
| 104 | { | ||
| 105 | if (now - it->second.windowStart < DEDUP_WINDOW_SECS) | ||
| 106 | { | ||
| 107 | ++it; | ||
| 108 | continue; | ||
| 109 | } | ||
| 110 | if (it->second.suppressed > 0) | ||
| 111 | { | ||
| 112 | LogDedupFlush f; | ||
| 113 | f.category = it->first.substr(0, it->first.find('|')); | ||
| 114 | f.suppressed = it->second.suppressed; | ||
| 115 | f.elapsed = now - it->second.windowStart; | ||
| 116 | f.lastMsg = it->second.lastMsg; | ||
| 117 | out.push_back(f); | ||
| 118 | } | ||
| 119 | g_logDedup.erase(it++); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 50 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) | 123 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) |
| 51 | { | 124 | { |
| 52 | // Call sites follow the existing printf() convention of ending their | 125 | // Call sites follow the existing printf() convention of ending their |
| @@ -61,11 +134,19 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 61 | 134 | ||
| 62 | int64 now = GetTime(); | 135 | int64 now = GetTime(); |
| 63 | bool fEmitNow = false; | 136 | bool fEmitNow = false; |
| 64 | int suppressedToFlush = 0; | 137 | std::vector<LogDedupFlush> flushes; |
| 65 | std::string flushedMsg; | ||
| 66 | 138 | ||
| 67 | CRITICAL_BLOCK(cs_logDedup) | 139 | CRITICAL_BLOCK(cs_logDedup) |
| 68 | { | 140 | { |
| 141 | // At most once a second. The table holds one entry per call site that | ||
| 142 | // has fired in the last few seconds, so the walk is short, but it must | ||
| 143 | // not run once per line of a burst. | ||
| 144 | if (now != g_logDedupLastSweep) | ||
| 145 | { | ||
| 146 | g_logDedupLastSweep = now; | ||
| 147 | SweepExpiredWindowsLocked(now, flushes); | ||
| 148 | } | ||
| 149 | |||
| 69 | std::map<std::string, LogDedupState>::iterator it = g_logDedup.find(key); | 150 | std::map<std::string, LogDedupState>::iterator it = g_logDedup.find(key); |
| 70 | if (it == g_logDedup.end()) | 151 | if (it == g_logDedup.end()) |
| 71 | { | 152 | { |
| @@ -76,10 +157,18 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 76 | } | 157 | } |
| 77 | else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) | 158 | else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) |
| 78 | { | 159 | { |
| 79 | // Window rolled over: flush what was suppressed during it (if | 160 | // Window rolled over within the same second as the last sweep: |
| 80 | // anything), then start a new window with this call printed. | 161 | // flush what was suppressed during it (if anything), then start a |
| 81 | suppressedToFlush = it->second.suppressed; | 162 | // new window with this call printed. |
| 82 | flushedMsg = it->second.lastMsg; | 163 | if (it->second.suppressed > 0) |
| 164 | { | ||
| 165 | LogDedupFlush f; | ||
| 166 | f.category = category; | ||
| 167 | f.suppressed = it->second.suppressed; | ||
| 168 | f.elapsed = now - it->second.windowStart; | ||
| 169 | f.lastMsg = it->second.lastMsg; | ||
| 170 | flushes.push_back(f); | ||
| 171 | } | ||
| 83 | it->second.windowStart = now; | 172 | it->second.windowStart = now; |
| 84 | it->second.suppressed = 0; | 173 | it->second.suppressed = 0; |
| 85 | it->second.lastMsg = msg; | 174 | it->second.lastMsg = msg; |
| @@ -94,13 +183,23 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: | |||
| 94 | } | 183 | } |
| 95 | } | 184 | } |
| 96 | 185 | ||
| 97 | if (suppressedToFlush > 0) | 186 | for (size_t i = 0; i < flushes.size(); i++) |
| 98 | OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", | 187 | EmitDedupSummary(flushes[i]); |
| 99 | category, suppressedToFlush, (long long)DEDUP_WINDOW_SECS, flushedMsg.c_str()); | ||
| 100 | if (fEmitNow) | 188 | if (fEmitNow) |
| 101 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); | 189 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); |
| 102 | } | 190 | } |
| 103 | 191 | ||
| 192 | // LogPrintAlways() -- see the doc comment in util.h. No window, no counting, | ||
| 193 | // no table entry: every call writes its own line. | ||
| 194 | void LogPrintNoCollapse(const char* category, const std::string& msgIn) | ||
| 195 | { | ||
| 196 | std::string msg = msgIn; | ||
| 197 | while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) | ||
| 198 | msg.pop_back(); | ||
| 199 | |||
| 200 | OutputDebugStringF("[%s] %s\n", category, msg.c_str()); | ||
| 201 | } | ||
| 202 | |||
| 104 | 203 | ||
| 105 | 204 | ||
| 106 | 205 | ||
| @@ -397,13 +496,37 @@ bool FileExists(const char* psz) | |||
| 397 | #endif | 496 | #endif |
| 398 | } | 497 | } |
| 399 | 498 | ||
| 400 | int GetFilesize(FILE* file) | 499 | #ifndef _WIN32 |
| 500 | // If off_t is 32 bits wide, ftello() truncates exactly like the int this | ||
| 501 | // function used to return. Fail the build rather than ship the silent | ||
| 502 | // version of the bug (32-bit hosts need -D_FILE_OFFSET_BITS=64). | ||
| 503 | static_assert(sizeof(off_t) >= 8, "GetFilesize needs a 64-bit off_t"); | ||
| 504 | #endif | ||
| 505 | |||
| 506 | // Returns the file's size in bytes, or -1 if it cannot be determined. | ||
| 507 | // | ||
| 508 | // This used to hold ftell()'s long in an int, so a file of 2 GiB or more came | ||
| 509 | // back negative. Its only caller is the debug.log roll in util.h, which skips | ||
| 510 | // the roll on a negative size -- i.e. rotation switched itself off for good at | ||
| 511 | // exactly the size the roll exists to prevent, and any cap of 2048 MB or more | ||
| 512 | // could never be exceeded at all. Both failures were silent. | ||
| 513 | int64 GetFilesize(FILE* file) | ||
| 401 | { | 514 | { |
| 402 | int nSavePos = ftell(file); | 515 | #ifdef _WIN32 |
| 403 | int nFilesize = -1; | 516 | __int64 nSavePos = _ftelli64(file); |
| 404 | if (fseek(file, 0, SEEK_END) == 0) | 517 | int64 nFilesize = -1; |
| 405 | nFilesize = ftell(file); | 518 | if (nSavePos >= 0 && _fseeki64(file, 0, SEEK_END) == 0) |
| 406 | fseek(file, nSavePos, SEEK_SET); | 519 | nFilesize = _ftelli64(file); |
| 520 | if (nSavePos >= 0) | ||
| 521 | _fseeki64(file, nSavePos, SEEK_SET); | ||
| 522 | #else | ||
| 523 | off_t nSavePos = ftello(file); | ||
| 524 | int64 nFilesize = -1; | ||
| 525 | if (nSavePos >= 0 && fseeko(file, 0, SEEK_END) == 0) | ||
| 526 | nFilesize = ftello(file); | ||
| 527 | if (nSavePos >= 0) | ||
| 528 | fseeko(file, nSavePos, SEEK_SET); | ||
| 529 | #endif | ||
| 407 | return nFilesize; | 530 | return nFilesize; |
| 408 | } | 531 | } |
| 409 | 532 | ||
| @@ -87,6 +87,15 @@ inline T& REF(const T& val) | |||
| 87 | 87 | ||
| 88 | extern bool fDebug; | 88 | extern bool fDebug; |
| 89 | 89 | ||
| 90 | // /debugrelay: the per-message firehose ("relay" and "nostrdiscovery" | ||
| 91 | // categories) that even -debug keeps off. See LogAcceptsCategory(). | ||
| 92 | extern bool fDebugRelay; | ||
| 93 | |||
| 94 | // debug.log is rolled to debug.log.old once it grows past this many bytes | ||
| 95 | // (set from -maxdebugfilesize, in MB; 0 disables the check). See the roll | ||
| 96 | // site in OutputDebugStringF() below. | ||
| 97 | extern int64 nMaxDebugFileSize; | ||
| 98 | |||
| 90 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
| 91 | // Categorized, burst-collapsing logging | 100 | // Categorized, burst-collapsing logging |
| 92 | // | 101 | // |
| @@ -106,15 +115,29 @@ extern bool fDebug; | |||
| 106 | // 2. Burst collapse: repeated LogPrint() calls from the *same source line* | 115 | // 2. Burst collapse: repeated LogPrint() calls from the *same source line* |
| 107 | // only emit one line per DEDUP_WINDOW_SECS; if more calls land inside | 116 | // only emit one line per DEDUP_WINDOW_SECS; if more calls land inside |
| 108 | // that window they're counted instead of reprinted, and the count is | 117 | // that window they're counted instead of reprinted, and the count is |
| 109 | // flushed as a single "(xN similar in Ws)" line the moment the window | 118 | // flushed as a single "(xN similar in Ws)" line once the window expires |
| 110 | // rolls over. A loop discovering 500 peers by address now produces a | 119 | // -- whether or not that call site ever fires again. A loop discovering |
| 111 | // small, bounded number of lines instead of 500 -- log growth is capped | 120 | // 500 peers by address now produces a small, bounded number of lines |
| 112 | // by elapsed time, not by how many similar events happen to fire. | 121 | // instead of 500 -- log growth is capped by elapsed time, not by how |
| 122 | // many similar events happen to fire. | ||
| 123 | // | ||
| 124 | // Use LogPrintAlways() instead for anything that reports an irreversible | ||
| 125 | // change to money: an obligation voided, a payment sent, a payment that | ||
| 126 | // failed. One line standing in for an unknown number of identical events is | ||
| 127 | // a tidy report of peer chatter and a false report of a ledger. A reorg | ||
| 128 | // voiding 100 entries hits one source line 100 times inside one window; | ||
| 129 | // under LogPrint() that read as a single block losing a single 49.49 BTF | ||
| 130 | // payment, while 4,950 BTF of obligations were actually destroyed, and the | ||
| 131 | // discrepancy was only found by counting entries in pending_payouts.json | ||
| 132 | // (issue #3). Bounded log growth is not worth that ambiguity here. | ||
| 113 | // --------------------------------------------------------------------------- | 133 | // --------------------------------------------------------------------------- |
| 114 | bool LogAcceptsCategory(const char* category); | 134 | bool LogAcceptsCategory(const char* category); |
| 115 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); | 135 | void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); |
| 136 | void LogPrintNoCollapse(const char* category, const std::string& msg); | ||
| 116 | #define LogPrint(category, ...) \ | 137 | #define LogPrint(category, ...) \ |
| 117 | do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) | 138 | do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) |
| 139 | #define LogPrintAlways(category, ...) \ | ||
| 140 | do { if (LogAcceptsCategory(category)) LogPrintNoCollapse((category), strprintf(__VA_ARGS__)); } while (0) | ||
| 118 | 141 | ||
| 119 | // Every format string in this project goes through one of the four functions | 142 | // Every format string in this project goes through one of the four functions |
| 120 | // below -- `printf` itself is remapped to OutputDebugStringF just above, and | 143 | // below -- `printf` itself is remapped to OutputDebugStringF just above, and |
| @@ -145,7 +168,7 @@ void ParseString(const string& str, char c, vector<string>& v); | |||
| 145 | string FormatMoney(int64 n, bool fPlus=false); | 168 | string FormatMoney(int64 n, bool fPlus=false); |
| 146 | bool ParseMoney(const char* pszIn, int64& nRet); | 169 | bool ParseMoney(const char* pszIn, int64& nRet); |
| 147 | bool FileExists(const char* psz); | 170 | bool FileExists(const char* psz); |
| 148 | int GetFilesize(FILE* file); | 171 | int64 GetFilesize(FILE* file); |
| 149 | uint64 GetRand(uint64 nMax); | 172 | uint64 GetRand(uint64 nMax); |
| 150 | int64 GetTime(); | 173 | int64 GetTime(); |
| 151 | int64 GetAdjustedTime(); | 174 | int64 GetAdjustedTime(); |
| @@ -199,7 +222,7 @@ protected: | |||
| 199 | public: | 222 | public: |
| 200 | char* pszFile; | 223 | char* pszFile; |
| 201 | int nLine; | 224 | int nLine; |
| 202 | explicit CCriticalSection() { InitializeCriticalSection(&cs); } | 225 | explicit CCriticalSection() : pszFile(NULL), nLine(0) { InitializeCriticalSection(&cs); } |
| 203 | ~CCriticalSection() { DeleteCriticalSection(&cs); } | 226 | ~CCriticalSection() { DeleteCriticalSection(&cs); } |
| 204 | void Enter() { EnterCriticalSection(&cs); } | 227 | void Enter() { EnterCriticalSection(&cs); } |
| 205 | void Leave() { LeaveCriticalSection(&cs); } | 228 | void Leave() { LeaveCriticalSection(&cs); } |
| @@ -390,7 +413,65 @@ inline int OutputDebugStringF(const char* pszFormat, ...) | |||
| 390 | else | 413 | else |
| 391 | strDebugFile = "debug.log"; | 414 | strDebugFile = "debug.log"; |
| 392 | } | 415 | } |
| 416 | // Latched by a failed rename in the roll below, which cannot be | ||
| 417 | // retried usefully and must not be retried endlessly. | ||
| 418 | static bool fRollDisabled = false; | ||
| 393 | FILE* fileout = fopen(strDebugFile.c_str(), "a"); | 419 | FILE* fileout = fopen(strDebugFile.c_str(), "a"); |
| 420 | if (fileout && nMaxDebugFileSize > 0 && !fRollDisabled) | ||
| 421 | { | ||
| 422 | // Roll an overgrown debug.log before adding to it. Nothing in the | ||
| 423 | // tree shrinks this file once the process is running (there is no | ||
| 424 | // ShrinkDebugFile, and even Bitcoin's only ran at startup), so a | ||
| 425 | // node that stays up for months grows the log without bound -- | ||
| 426 | // production hit ~2 GiB. The file is opened and closed per line, | ||
| 427 | // so a rename is safe here: the next line simply reopens the | ||
| 428 | // path. One generation is kept as debug.log.old; logrotate | ||
| 429 | // (contrib/logrotate/) handles anything longer-term. | ||
| 430 | int64 nSize = GetFilesize(fileout); | ||
| 431 | if (nSize >= 0 && nSize > nMaxDebugFileSize) | ||
| 432 | { | ||
| 433 | fclose(fileout); | ||
| 434 | string strOldFile = strDebugFile + ".old"; | ||
| 435 | remove(strOldFile.c_str()); // rename() won't clobber on Windows | ||
| 436 | bool fRolled = (rename(strDebugFile.c_str(), strOldFile.c_str()) == 0); | ||
| 437 | int nRenameErrno = errno; // before fopen() can overwrite it | ||
| 438 | fileout = fopen(strDebugFile.c_str(), "a"); | ||
| 439 | if (fRolled) | ||
| 440 | { | ||
| 441 | if (fileout) | ||
| 442 | fprintf(fileout, | ||
| 443 | "debug.log grew past %lld MB; previous contents moved to debug.log.old (-maxdebugfilesize)\n", | ||
| 444 | (long long)(nMaxDebugFileSize / 1024 / 1024)); | ||
| 445 | } | ||
| 446 | else if (nRenameErrno == ENOENT) | ||
| 447 | { | ||
| 448 | // The file went away between the size check and the | ||
| 449 | // rename, so an external rotator (logrotate) got there | ||
| 450 | // first. Nothing left to roll and nothing wrong: the | ||
| 451 | // fopen above has already recreated it. Latching on this | ||
| 452 | // would let a benign race with logrotate switch the cap | ||
| 453 | // off for the rest of the run. | ||
| 454 | } | ||
| 455 | else | ||
| 456 | { | ||
| 457 | // The file is still over the cap, so this branch would be | ||
| 458 | // reached again on the next line and every line after it. | ||
| 459 | // Unlatched, the complaint about failing to roll becomes | ||
| 460 | // the runaway growth the roll exists to prevent. Say it | ||
| 461 | // once and stop trying -- on stderr too, because whatever | ||
| 462 | // blocked the rename (read-only mount, permissions, EXDEV | ||
| 463 | // across a bind mount) may equally block the log write. | ||
| 464 | fRollDisabled = true; | ||
| 465 | fprintf(stderr, | ||
| 466 | "cannot roll %s (rename to .old failed: %s); -maxdebugfilesize disabled for this run\n", | ||
| 467 | strDebugFile.c_str(), strerror(nRenameErrno)); | ||
| 468 | if (fileout) | ||
| 469 | fprintf(fileout, | ||
| 470 | "cannot roll debug.log (rename to .old failed: %s); -maxdebugfilesize disabled for this run\n", | ||
| 471 | strerror(nRenameErrno)); | ||
| 472 | } | ||
| 473 | } | ||
| 474 | } | ||
| 394 | if (fileout) | 475 | if (fileout) |
| 395 | { | 476 | { |
| 396 | va_list arg_ptr; | 477 | va_list arg_ptr; |
diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp new file mode 100644 index 0000000..f4b4b37 --- /dev/null +++ b/src/wallet_sqlite.cpp | |||
| @@ -0,0 +1,374 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | |||
| 4 | #include "headers.h" | ||
| 5 | #include "wallet_sqlite.h" | ||
| 6 | |||
| 7 | static void SetSQLiteError(sqlite3* pdb, const string& strContext, | ||
| 8 | string& strError) | ||
| 9 | { | ||
| 10 | if (pdb) | ||
| 11 | strError = strContext + ": " + sqlite3_errmsg(pdb); | ||
| 12 | else | ||
| 13 | strError = strContext; | ||
| 14 | } | ||
| 15 | |||
| 16 | CWalletDBSQLite::CWalletDBSQLite() : pdb(NULL) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | CWalletDBSQLite::~CWalletDBSQLite() | ||
| 21 | { | ||
| 22 | Close(); | ||
| 23 | } | ||
| 24 | |||
| 25 | bool CWalletDBSQLite::Open(const string& strPath, string& strError) | ||
| 26 | { | ||
| 27 | Close(); | ||
| 28 | |||
| 29 | int ret = sqlite3_open_v2(strPath.c_str(), &pdb, | ||
| 30 | SQLITE_OPEN_READWRITE | | ||
| 31 | SQLITE_OPEN_CREATE | | ||
| 32 | SQLITE_OPEN_FULLMUTEX, | ||
| 33 | NULL); | ||
| 34 | if (ret != SQLITE_OK) | ||
| 35 | { | ||
| 36 | SetSQLiteError(pdb, "cannot open SQLite wallet", strError); | ||
| 37 | Close(); | ||
| 38 | return false; | ||
| 39 | } | ||
| 40 | |||
| 41 | sqlite3_busy_timeout(pdb, 5000); | ||
| 42 | |||
| 43 | if (!Exec("PRAGMA journal_mode=WAL;", strError)) | ||
| 44 | { | ||
| 45 | Close(); | ||
| 46 | return false; | ||
| 47 | } | ||
| 48 | if (!Exec("PRAGMA synchronous=FULL;", strError)) | ||
| 49 | { | ||
| 50 | Close(); | ||
| 51 | return false; | ||
| 52 | } | ||
| 53 | if (!InitSchema(strError)) | ||
| 54 | { | ||
| 55 | Close(); | ||
| 56 | return false; | ||
| 57 | } | ||
| 58 | return true; | ||
| 59 | } | ||
| 60 | |||
| 61 | bool CWalletDBSQLite::OpenReadOnly(const string& strPath, string& strError) | ||
| 62 | { | ||
| 63 | Close(); | ||
| 64 | |||
| 65 | int ret = sqlite3_open_v2(strPath.c_str(), &pdb, | ||
| 66 | SQLITE_OPEN_READONLY | | ||
| 67 | SQLITE_OPEN_FULLMUTEX, | ||
| 68 | NULL); | ||
| 69 | if (ret != SQLITE_OK) | ||
| 70 | { | ||
| 71 | SetSQLiteError(pdb, "cannot open SQLite wallet read-only", strError); | ||
| 72 | Close(); | ||
| 73 | return false; | ||
| 74 | } | ||
| 75 | |||
| 76 | sqlite3_busy_timeout(pdb, 5000); | ||
| 77 | return true; | ||
| 78 | } | ||
| 79 | |||
| 80 | void CWalletDBSQLite::Close() | ||
| 81 | { | ||
| 82 | if (pdb) | ||
| 83 | { | ||
| 84 | sqlite3_close(pdb); | ||
| 85 | pdb = NULL; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | bool CWalletDBSQLite::BeginTransaction(string& strError) | ||
| 90 | { | ||
| 91 | if (!pdb) | ||
| 92 | { | ||
| 93 | strError = "SQLite wallet is not open"; | ||
| 94 | return false; | ||
| 95 | } | ||
| 96 | return Exec("BEGIN IMMEDIATE;", strError); | ||
| 97 | } | ||
| 98 | |||
| 99 | bool CWalletDBSQLite::CommitTransaction(string& strError) | ||
| 100 | { | ||
| 101 | if (!pdb) | ||
| 102 | { | ||
| 103 | strError = "SQLite wallet is not open"; | ||
| 104 | return false; | ||
| 105 | } | ||
| 106 | return Exec("COMMIT;", strError); | ||
| 107 | } | ||
| 108 | |||
| 109 | bool CWalletDBSQLite::RollbackTransaction(string& strError) | ||
| 110 | { | ||
| 111 | if (!pdb) | ||
| 112 | { | ||
| 113 | strError = "SQLite wallet is not open"; | ||
| 114 | return false; | ||
| 115 | } | ||
| 116 | return Exec("ROLLBACK;", strError); | ||
| 117 | } | ||
| 118 | |||
| 119 | bool CWalletDBSQLite::Checkpoint(string& strError) | ||
| 120 | { | ||
| 121 | if (!pdb) | ||
| 122 | { | ||
| 123 | strError = "SQLite wallet is not open"; | ||
| 124 | return false; | ||
| 125 | } | ||
| 126 | return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError); | ||
| 127 | } | ||
| 128 | |||
| 129 | bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) | ||
| 130 | { | ||
| 131 | char* pszErr = NULL; | ||
| 132 | int ret = sqlite3_exec(pdb, pszSql, NULL, NULL, &pszErr); | ||
| 133 | if (ret != SQLITE_OK) | ||
| 134 | { | ||
| 135 | if (pszErr) | ||
| 136 | { | ||
| 137 | strError = pszErr; | ||
| 138 | sqlite3_free(pszErr); | ||
| 139 | } | ||
| 140 | else | ||
| 141 | SetSQLiteError(pdb, "SQLite exec failed", strError); | ||
| 142 | return false; | ||
| 143 | } | ||
| 144 | return true; | ||
| 145 | } | ||
| 146 | |||
| 147 | bool CWalletDBSQLite::InitSchema(string& strError) | ||
| 148 | { | ||
| 149 | return Exec("CREATE TABLE IF NOT EXISTS wallet_records (" | ||
| 150 | "key BLOB PRIMARY KEY NOT NULL," | ||
| 151 | "value BLOB NOT NULL" | ||
| 152 | ");", | ||
| 153 | strError) && | ||
| 154 | Exec("PRAGMA user_version=1;", strError); | ||
| 155 | } | ||
| 156 | |||
| 157 | bool CWalletDBSQLite::WriteRecord(const vector<unsigned char>& vchKey, | ||
| 158 | const vector<unsigned char>& vchValue, | ||
| 159 | string& strError, | ||
| 160 | bool fOverwrite) | ||
| 161 | { | ||
| 162 | if (!pdb) | ||
| 163 | { | ||
| 164 | strError = "SQLite wallet is not open"; | ||
| 165 | return false; | ||
| 166 | } | ||
| 167 | if (vchKey.empty()) | ||
| 168 | { | ||
| 169 | strError = "SQLite wallet record key is empty"; | ||
| 170 | return false; | ||
| 171 | } | ||
| 172 | |||
| 173 | sqlite3_stmt* stmt = NULL; | ||
| 174 | const char* pszSql = fOverwrite | ||
| 175 | ? "INSERT OR REPLACE INTO wallet_records(key, value) VALUES(?, ?);" | ||
| 176 | : "INSERT INTO wallet_records(key, value) VALUES(?, ?);"; | ||
| 177 | int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); | ||
| 178 | if (ret != SQLITE_OK) | ||
| 179 | { | ||
| 180 | SetSQLiteError(pdb, "cannot prepare SQLite wallet write", strError); | ||
| 181 | return false; | ||
| 182 | } | ||
| 183 | |||
| 184 | ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), | ||
| 185 | SQLITE_TRANSIENT); | ||
| 186 | if (ret == SQLITE_OK) | ||
| 187 | ret = sqlite3_bind_blob(stmt, 2, | ||
| 188 | vchValue.empty() ? NULL : &vchValue[0], | ||
| 189 | (int)vchValue.size(), | ||
| 190 | SQLITE_TRANSIENT); | ||
| 191 | if (ret == SQLITE_OK) | ||
| 192 | ret = sqlite3_step(stmt); | ||
| 193 | |||
| 194 | bool fOk = (ret == SQLITE_DONE); | ||
| 195 | if (!fOk) | ||
| 196 | SetSQLiteError(pdb, "cannot write SQLite wallet record", strError); | ||
| 197 | sqlite3_finalize(stmt); | ||
| 198 | return fOk; | ||
| 199 | } | ||
| 200 | |||
| 201 | bool CWalletDBSQLite::ReadRecord(const vector<unsigned char>& vchKey, | ||
| 202 | vector<unsigned char>& vchValueRet, | ||
| 203 | string& strError) | ||
| 204 | { | ||
| 205 | vchValueRet.clear(); | ||
| 206 | if (!pdb) | ||
| 207 | { | ||
| 208 | strError = "SQLite wallet is not open"; | ||
| 209 | return false; | ||
| 210 | } | ||
| 211 | if (vchKey.empty()) | ||
| 212 | { | ||
| 213 | strError = "SQLite wallet record key is empty"; | ||
| 214 | return false; | ||
| 215 | } | ||
| 216 | |||
| 217 | sqlite3_stmt* stmt = NULL; | ||
| 218 | const char* pszSql = "SELECT value FROM wallet_records WHERE key = ?;"; | ||
| 219 | int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); | ||
| 220 | if (ret != SQLITE_OK) | ||
| 221 | { | ||
| 222 | SetSQLiteError(pdb, "cannot prepare SQLite wallet read", strError); | ||
| 223 | return false; | ||
| 224 | } | ||
| 225 | |||
| 226 | ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), | ||
| 227 | SQLITE_TRANSIENT); | ||
| 228 | if (ret != SQLITE_OK) | ||
| 229 | { | ||
| 230 | SetSQLiteError(pdb, "cannot bind SQLite wallet key", strError); | ||
| 231 | sqlite3_finalize(stmt); | ||
| 232 | return false; | ||
| 233 | } | ||
| 234 | |||
| 235 | ret = sqlite3_step(stmt); | ||
| 236 | if (ret == SQLITE_ROW) | ||
| 237 | { | ||
| 238 | const unsigned char* pch = | ||
| 239 | (const unsigned char*)sqlite3_column_blob(stmt, 0); | ||
| 240 | int nBytes = sqlite3_column_bytes(stmt, 0); | ||
| 241 | if (nBytes > 0 && pch) | ||
| 242 | vchValueRet.assign(pch, pch + nBytes); | ||
| 243 | sqlite3_finalize(stmt); | ||
| 244 | return true; | ||
| 245 | } | ||
| 246 | |||
| 247 | if (ret == SQLITE_DONE) | ||
| 248 | strError = "SQLite wallet record not found"; | ||
| 249 | else | ||
| 250 | SetSQLiteError(pdb, "cannot read SQLite wallet record", strError); | ||
| 251 | sqlite3_finalize(stmt); | ||
| 252 | return false; | ||
| 253 | } | ||
| 254 | |||
| 255 | bool CWalletDBSQLite::EraseRecord(const vector<unsigned char>& vchKey, | ||
| 256 | string& strError) | ||
| 257 | { | ||
| 258 | if (!pdb) | ||
| 259 | { | ||
| 260 | strError = "SQLite wallet is not open"; | ||
| 261 | return false; | ||
| 262 | } | ||
| 263 | if (vchKey.empty()) | ||
| 264 | { | ||
| 265 | strError = "SQLite wallet record key is empty"; | ||
| 266 | return false; | ||
| 267 | } | ||
| 268 | |||
| 269 | sqlite3_stmt* stmt = NULL; | ||
| 270 | const char* pszSql = "DELETE FROM wallet_records WHERE key = ?;"; | ||
| 271 | int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); | ||
| 272 | if (ret != SQLITE_OK) | ||
| 273 | { | ||
| 274 | SetSQLiteError(pdb, "cannot prepare SQLite wallet erase", strError); | ||
| 275 | return false; | ||
| 276 | } | ||
| 277 | |||
| 278 | ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), | ||
| 279 | SQLITE_TRANSIENT); | ||
| 280 | if (ret == SQLITE_OK) | ||
| 281 | ret = sqlite3_step(stmt); | ||
| 282 | |||
| 283 | bool fOk = (ret == SQLITE_DONE); | ||
| 284 | if (!fOk) | ||
| 285 | SetSQLiteError(pdb, "cannot erase SQLite wallet record", strError); | ||
| 286 | sqlite3_finalize(stmt); | ||
| 287 | return fOk; | ||
| 288 | } | ||
| 289 | |||
| 290 | bool CWalletDBSQLite::ScanRecords(CWalletRecordVisitor& visitor, | ||
| 291 | string& strError) | ||
| 292 | { | ||
| 293 | if (!pdb) | ||
| 294 | { | ||
| 295 | strError = "SQLite wallet is not open"; | ||
| 296 | return false; | ||
| 297 | } | ||
| 298 | |||
| 299 | sqlite3_stmt* stmt = NULL; | ||
| 300 | const char* pszSql = "SELECT key, value FROM wallet_records ORDER BY rowid;"; | ||
| 301 | int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); | ||
| 302 | if (ret != SQLITE_OK) | ||
| 303 | { | ||
| 304 | SetSQLiteError(pdb, "cannot prepare SQLite wallet scan", strError); | ||
| 305 | return false; | ||
| 306 | } | ||
| 307 | |||
| 308 | while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) | ||
| 309 | { | ||
| 310 | const unsigned char* pchKey = | ||
| 311 | (const unsigned char*)sqlite3_column_blob(stmt, 0); | ||
| 312 | int nKeyBytes = sqlite3_column_bytes(stmt, 0); | ||
| 313 | const unsigned char* pchValue = | ||
| 314 | (const unsigned char*)sqlite3_column_blob(stmt, 1); | ||
| 315 | int nValueBytes = sqlite3_column_bytes(stmt, 1); | ||
| 316 | |||
| 317 | vector<unsigned char> vchKey; | ||
| 318 | vector<unsigned char> vchValue; | ||
| 319 | if (nKeyBytes > 0 && pchKey) | ||
| 320 | vchKey.assign(pchKey, pchKey + nKeyBytes); | ||
| 321 | if (nValueBytes > 0 && pchValue) | ||
| 322 | vchValue.assign(pchValue, pchValue + nValueBytes); | ||
| 323 | |||
| 324 | CDataStream ssKey(vchKey, SER_DISK); | ||
| 325 | CDataStream ssValue(vchValue, SER_DISK); | ||
| 326 | if (!visitor.VisitWalletRecord(ssKey, ssValue, strError)) | ||
| 327 | { | ||
| 328 | sqlite3_finalize(stmt); | ||
| 329 | return false; | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | if (ret != SQLITE_DONE) | ||
| 334 | { | ||
| 335 | SetSQLiteError(pdb, "cannot scan SQLite wallet records", strError); | ||
| 336 | sqlite3_finalize(stmt); | ||
| 337 | return false; | ||
| 338 | } | ||
| 339 | |||
| 340 | sqlite3_finalize(stmt); | ||
| 341 | return true; | ||
| 342 | } | ||
| 343 | |||
| 344 | bool CWalletDBSQLite::CountRecords(int& nRecordsRet, string& strError) | ||
| 345 | { | ||
| 346 | nRecordsRet = 0; | ||
| 347 | if (!pdb) | ||
| 348 | { | ||
| 349 | strError = "SQLite wallet is not open"; | ||
| 350 | return false; | ||
| 351 | } | ||
| 352 | |||
| 353 | sqlite3_stmt* stmt = NULL; | ||
| 354 | int ret = sqlite3_prepare_v2(pdb, | ||
| 355 | "SELECT COUNT(*) FROM wallet_records;", | ||
| 356 | -1, &stmt, NULL); | ||
| 357 | if (ret != SQLITE_OK) | ||
| 358 | { | ||
| 359 | SetSQLiteError(pdb, "cannot prepare SQLite wallet count", strError); | ||
| 360 | return false; | ||
| 361 | } | ||
| 362 | |||
| 363 | ret = sqlite3_step(stmt); | ||
| 364 | if (ret != SQLITE_ROW) | ||
| 365 | { | ||
| 366 | SetSQLiteError(pdb, "cannot count SQLite wallet records", strError); | ||
| 367 | sqlite3_finalize(stmt); | ||
| 368 | return false; | ||
| 369 | } | ||
| 370 | |||
| 371 | nRecordsRet = sqlite3_column_int(stmt, 0); | ||
| 372 | sqlite3_finalize(stmt); | ||
| 373 | return true; | ||
| 374 | } | ||
diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h new file mode 100644 index 0000000..d27e13a --- /dev/null +++ b/src/wallet_sqlite.h | |||
| @@ -0,0 +1,120 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | |||
| 4 | #ifndef BITFLASH_WALLET_SQLITE_H | ||
| 5 | #define BITFLASH_WALLET_SQLITE_H | ||
| 6 | |||
| 7 | #include <sqlite3.h> | ||
| 8 | |||
| 9 | class CWalletRecordVisitor; | ||
| 10 | |||
| 11 | class CWalletDBSQLite | ||
| 12 | { | ||
| 13 | public: | ||
| 14 | CWalletDBSQLite(); | ||
| 15 | ~CWalletDBSQLite(); | ||
| 16 | |||
| 17 | bool Open(const string& strPath, string& strError); | ||
| 18 | bool OpenReadOnly(const string& strPath, string& strError); | ||
| 19 | void Close(); | ||
| 20 | |||
| 21 | bool BeginTransaction(string& strError); | ||
| 22 | bool CommitTransaction(string& strError); | ||
| 23 | bool RollbackTransaction(string& strError); | ||
| 24 | bool Checkpoint(string& strError); | ||
| 25 | |||
| 26 | bool WriteRecord(const vector<unsigned char>& vchKey, | ||
| 27 | const vector<unsigned char>& vchValue, | ||
| 28 | string& strError, | ||
| 29 | bool fOverwrite=true); | ||
| 30 | bool ReadRecord(const vector<unsigned char>& vchKey, | ||
| 31 | vector<unsigned char>& vchValueRet, | ||
| 32 | string& strError); | ||
| 33 | bool EraseRecord(const vector<unsigned char>& vchKey, | ||
| 34 | string& strError); | ||
| 35 | bool ScanRecords(CWalletRecordVisitor& visitor, string& strError); | ||
| 36 | bool CountRecords(int& nRecordsRet, string& strError); | ||
| 37 | |||
| 38 | template<typename K, typename T> | ||
| 39 | bool WriteTypedRecord(const K& key, const T& value, string& strError, | ||
| 40 | bool fOverwrite=true) | ||
| 41 | { | ||
| 42 | CDataStream ssKey(SER_DISK); | ||
| 43 | ssKey.reserve(1000); | ||
| 44 | ssKey << key; | ||
| 45 | |||
| 46 | CDataStream ssValue(SER_DISK); | ||
| 47 | ssValue.reserve(10000); | ||
| 48 | ssValue << value; | ||
| 49 | |||
| 50 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 51 | vector<unsigned char> vchValue(ssValue.begin(), ssValue.end()); | ||
| 52 | bool fOk = WriteRecord(vchKey, vchValue, strError, fOverwrite); | ||
| 53 | |||
| 54 | if (!vchKey.empty()) | ||
| 55 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 56 | if (!vchValue.empty()) | ||
| 57 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 58 | if (!ssKey.empty()) | ||
| 59 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 60 | if (!ssValue.empty()) | ||
| 61 | memset(&ssValue[0], 0, ssValue.size()); | ||
| 62 | return fOk; | ||
| 63 | } | ||
| 64 | |||
| 65 | template<typename K, typename T> | ||
| 66 | bool ReadTypedRecord(const K& key, T& valueRet, string& strError) | ||
| 67 | { | ||
| 68 | CDataStream ssKey(SER_DISK); | ||
| 69 | ssKey.reserve(1000); | ||
| 70 | ssKey << key; | ||
| 71 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 72 | |||
| 73 | vector<unsigned char> vchValue; | ||
| 74 | bool fOk = ReadRecord(vchKey, vchValue, strError); | ||
| 75 | if (!vchKey.empty()) | ||
| 76 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 77 | if (!ssKey.empty()) | ||
| 78 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 79 | if (!fOk) | ||
| 80 | return false; | ||
| 81 | |||
| 82 | CDataStream ssValue(vchValue, SER_DISK); | ||
| 83 | ssValue >> valueRet; | ||
| 84 | bool fReadOk = !ssValue.fail(); | ||
| 85 | if (!vchValue.empty()) | ||
| 86 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 87 | if (!ssValue.empty()) | ||
| 88 | memset(&ssValue[0], 0, ssValue.size()); | ||
| 89 | if (!fReadOk) | ||
| 90 | strError = "SQLite wallet record could not be deserialized"; | ||
| 91 | return fReadOk; | ||
| 92 | } | ||
| 93 | |||
| 94 | template<typename K> | ||
| 95 | bool EraseTypedRecord(const K& key, string& strError) | ||
| 96 | { | ||
| 97 | CDataStream ssKey(SER_DISK); | ||
| 98 | ssKey.reserve(1000); | ||
| 99 | ssKey << key; | ||
| 100 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 101 | |||
| 102 | bool fOk = EraseRecord(vchKey, strError); | ||
| 103 | if (!vchKey.empty()) | ||
| 104 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 105 | if (!ssKey.empty()) | ||
| 106 | memset(&ssKey[0], 0, ssKey.size()); | ||
| 107 | return fOk; | ||
| 108 | } | ||
| 109 | |||
| 110 | private: | ||
| 111 | sqlite3* pdb; | ||
| 112 | |||
| 113 | bool Exec(const char* pszSql, string& strError); | ||
| 114 | bool InitSchema(string& strError); | ||
| 115 | |||
| 116 | CWalletDBSQLite(const CWalletDBSQLite&); | ||
| 117 | void operator=(const CWalletDBSQLite&); | ||
| 118 | }; | ||
| 119 | |||
| 120 | #endif | ||
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index e8ac4bf..f59fc0c 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -12,6 +12,7 @@ | |||
| 12 | #include "headers_core.h" | 12 | #include "headers_core.h" |
| 13 | #include "bip32.h" | 13 | #include "bip32.h" |
| 14 | #include "walletcmd.h" | 14 | #include "walletcmd.h" |
| 15 | #include "wallet_sqlite.h" | ||
| 15 | 16 | ||
| 16 | #undef printf | 17 | #undef printf |
| 17 | 18 | ||
| @@ -400,7 +401,12 @@ int CmdRecoveryAudit() | |||
| 400 | int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit; | 401 | int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit; |
| 401 | 402 | ||
| 402 | printf("Wallet recovery audit\n"); | 403 | printf("Wallet recovery audit\n"); |
| 403 | printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); | 404 | if (audit.fHaveSeed) |
| 405 | printf(" recovery phrase: present\n"); | ||
| 406 | else if (audit.fSeedEncryptedLocked) | ||
| 407 | printf(" recovery phrase: encrypted, unlock wallet to audit\n"); | ||
| 408 | else | ||
| 409 | printf(" recovery phrase: not installed\n"); | ||
| 404 | if (audit.fHaveSeed) | 410 | if (audit.fHaveSeed) |
| 405 | { | 411 | { |
| 406 | printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); | 412 | printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); |
| @@ -412,15 +418,24 @@ int CmdRecoveryAudit() | |||
| 412 | if (!audit.fDeriveComplete) | 418 | if (!audit.fDeriveComplete) |
| 413 | printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); | 419 | printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); |
| 414 | printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); | 420 | printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); |
| 421 | if (audit.fSeedEncryptedLocked) | ||
| 422 | { | ||
| 423 | printf(" recovery coverage: unavailable while wallet is locked\n"); | ||
| 424 | printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); | ||
| 425 | printf("\n"); | ||
| 426 | printf("Unlock the wallet with /walletpassphrase or /walletpassphrase=@FILE to audit phrase coverage.\n"); | ||
| 427 | fflush(stdout); | ||
| 428 | return 2; | ||
| 429 | } | ||
| 415 | printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", | 430 | printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", |
| 416 | FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); | 431 | FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); |
| 417 | printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n", | 432 | printf(" not phrase-backed balance: %s BTF (%d transaction(s))\n", |
| 418 | FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); | 433 | FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); |
| 419 | printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); | 434 | printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); |
| 420 | printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", | 435 | printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", |
| 421 | FormatMoney(audit.nRecoverableImmatureCredit).c_str(), | 436 | FormatMoney(audit.nRecoverableImmatureCredit).c_str(), |
| 422 | audit.nRecoverableImmatureTx); | 437 | audit.nRecoverableImmatureTx); |
| 423 | printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n", | 438 | printf(" not phrase-backed immature: %s BTF (%d transaction(s))\n", |
| 424 | FormatMoney(audit.nLegacyImmatureCredit).c_str(), | 439 | FormatMoney(audit.nLegacyImmatureCredit).c_str(), |
| 425 | audit.nLegacyImmatureTx); | 440 | audit.nLegacyImmatureTx); |
| 426 | 441 | ||
| @@ -442,7 +457,7 @@ int CmdRecoveryAudit() | |||
| 442 | { | 457 | { |
| 443 | printf("\n"); | 458 | printf("\n"); |
| 444 | printf("Warning: some coins are on keys the phrase does not reproduce.\n"); | 459 | printf("Warning: some coins are on keys the phrase does not reproduce.\n"); |
| 445 | printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n"); | 460 | printf("Keep a file backup of this wallet until that balance has been moved to a phrase-backed address.\n"); |
| 446 | fflush(stdout); | 461 | fflush(stdout); |
| 447 | return 2; | 462 | return 2; |
| 448 | } | 463 | } |
| @@ -456,10 +471,952 @@ int CmdRecoveryAudit() | |||
| 456 | return 0; | 471 | return 0; |
| 457 | } | 472 | } |
| 458 | 473 | ||
| 474 | struct WalletStorageAuditCounts | ||
| 475 | { | ||
| 476 | unsigned int nTotal = 0; | ||
| 477 | unsigned int nMalformed = 0; | ||
| 478 | unsigned int nUnknown = 0; | ||
| 479 | unsigned int nVersion = 0; | ||
| 480 | unsigned int nNames = 0; | ||
| 481 | unsigned int nTransactions = 0; | ||
| 482 | unsigned int nPlainKeys = 0; | ||
| 483 | unsigned int nEncryptedKeys = 0; | ||
| 484 | unsigned int nMasterKeys = 0; | ||
| 485 | unsigned int nDefaultKey = 0; | ||
| 486 | unsigned int nPlainHDMaster = 0; | ||
| 487 | unsigned int nPlainHDChainCode = 0; | ||
| 488 | unsigned int nCryptedHDMaster = 0; | ||
| 489 | unsigned int nCryptedHDChainCode = 0; | ||
| 490 | unsigned int nWalletMinVersion = 0; | ||
| 491 | unsigned int nPool = 0; | ||
| 492 | unsigned int nSettings = 0; | ||
| 493 | unsigned int nHDNext = 0; | ||
| 494 | unsigned int nHDSchema = 0; | ||
| 495 | unsigned int nHDCoinType = 0; | ||
| 496 | unsigned int nHDReceiveNext = 0; | ||
| 497 | unsigned int nHDChangeNext = 0; | ||
| 498 | }; | ||
| 499 | |||
| 500 | static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain) | ||
| 501 | { | ||
| 502 | if (nMaster == 0 && nChain == 0) | ||
| 503 | return "none"; | ||
| 504 | if (nMaster > 0 && nChain > 0) | ||
| 505 | return "complete"; | ||
| 506 | return "incomplete"; | ||
| 507 | } | ||
| 508 | |||
| 509 | static void CountWalletStorageType(const std::string& strType, | ||
| 510 | WalletStorageAuditCounts& c) | ||
| 511 | { | ||
| 512 | if (strType == "version") | ||
| 513 | c.nVersion++; | ||
| 514 | else if (strType == "name") | ||
| 515 | c.nNames++; | ||
| 516 | else if (strType == "tx") | ||
| 517 | c.nTransactions++; | ||
| 518 | else if (strType == "key") | ||
| 519 | c.nPlainKeys++; | ||
| 520 | else if (strType == "ckey") | ||
| 521 | c.nEncryptedKeys++; | ||
| 522 | else if (strType == "mkey") | ||
| 523 | c.nMasterKeys++; | ||
| 524 | else if (strType == "defaultkey") | ||
| 525 | c.nDefaultKey++; | ||
| 526 | else if (strType == "hdmaster") | ||
| 527 | c.nPlainHDMaster++; | ||
| 528 | else if (strType == "hdchaincode") | ||
| 529 | c.nPlainHDChainCode++; | ||
| 530 | else if (strType == "cryptedhdmaster") | ||
| 531 | c.nCryptedHDMaster++; | ||
| 532 | else if (strType == "cryptedhdchaincode") | ||
| 533 | c.nCryptedHDChainCode++; | ||
| 534 | else if (strType == "walletminversion") | ||
| 535 | c.nWalletMinVersion++; | ||
| 536 | else if (strType == "pool") | ||
| 537 | c.nPool++; | ||
| 538 | else if (strType == "setting") | ||
| 539 | c.nSettings++; | ||
| 540 | else if (strType == "hdnext") | ||
| 541 | c.nHDNext++; | ||
| 542 | else if (strType == "hdschema") | ||
| 543 | c.nHDSchema++; | ||
| 544 | else if (strType == "hdcointype") | ||
| 545 | c.nHDCoinType++; | ||
| 546 | else if (strType == "hdreceivenext") | ||
| 547 | c.nHDReceiveNext++; | ||
| 548 | else if (strType == "hdchangenext") | ||
| 549 | c.nHDChangeNext++; | ||
| 550 | else | ||
| 551 | c.nUnknown++; | ||
| 552 | } | ||
| 553 | |||
| 554 | static bool ReadWalletStorageCounts(WalletStorageAuditCounts& counts, | ||
| 555 | std::string& strError) | ||
| 556 | { | ||
| 557 | class CWalletStorageCountVisitor : public CWalletRecordVisitor | ||
| 558 | { | ||
| 559 | public: | ||
| 560 | WalletStorageAuditCounts& counts; | ||
| 561 | |||
| 562 | explicit CWalletStorageCountVisitor(WalletStorageAuditCounts& countsIn) | ||
| 563 | : counts(countsIn) { } | ||
| 564 | |||
| 565 | bool VisitWalletRecord(const CDataStream& ssKeyIn, | ||
| 566 | const CDataStream& ssValue, | ||
| 567 | std::string& strErrorRet) | ||
| 568 | { | ||
| 569 | (void)ssValue; | ||
| 570 | (void)strErrorRet; | ||
| 571 | counts.nTotal++; | ||
| 572 | try | ||
| 573 | { | ||
| 574 | CDataStream ssKey = ssKeyIn; | ||
| 575 | std::string strType; | ||
| 576 | ssKey >> strType; | ||
| 577 | if (ssKey.fail()) | ||
| 578 | { | ||
| 579 | counts.nMalformed++; | ||
| 580 | return true; | ||
| 581 | } | ||
| 582 | CountWalletStorageType(strType, counts); | ||
| 583 | } | ||
| 584 | catch (...) | ||
| 585 | { | ||
| 586 | counts.nMalformed++; | ||
| 587 | } | ||
| 588 | return true; | ||
| 589 | } | ||
| 590 | }; | ||
| 591 | |||
| 592 | CWalletStorageCountVisitor visitor(counts); | ||
| 593 | return ScanWalletRecords(visitor, strError); | ||
| 594 | } | ||
| 595 | |||
| 596 | static std::string WalletStorageAuditJson(const WalletStorageAuditCounts& counts) | ||
| 597 | { | ||
| 598 | std::string strJson; | ||
| 599 | strJson += "{\n"; | ||
| 600 | strJson += " \"format\": \"bitflash-wallet-storage-audit-v1\",\n"; | ||
| 601 | strJson += strprintf(" \"records_total\": %u,\n", counts.nTotal); | ||
| 602 | strJson += strprintf(" \"malformed_records\": %u,\n", counts.nMalformed); | ||
| 603 | strJson += strprintf(" \"unknown_records\": %u,\n", counts.nUnknown); | ||
| 604 | strJson += strprintf(" \"plain_private_keys\": %u,\n", counts.nPlainKeys); | ||
| 605 | strJson += strprintf(" \"encrypted_private_keys\": %u,\n", counts.nEncryptedKeys); | ||
| 606 | strJson += strprintf(" \"encryption_master_keys\": %u,\n", counts.nMasterKeys); | ||
| 607 | strJson += strprintf(" \"default_public_key\": \"%s\",\n", | ||
| 608 | counts.nDefaultKey ? "present" : "none"); | ||
| 609 | strJson += strprintf(" \"plain_hd_seed\": \"%s\",\n", | ||
| 610 | HDSeedStorageState(counts.nPlainHDMaster, | ||
| 611 | counts.nPlainHDChainCode).c_str()); | ||
| 612 | strJson += strprintf(" \"encrypted_hd_seed\": \"%s\",\n", | ||
| 613 | HDSeedStorageState(counts.nCryptedHDMaster, | ||
| 614 | counts.nCryptedHDChainCode).c_str()); | ||
| 615 | strJson += strprintf(" \"wallet_minimum_version\": \"%s\",\n", | ||
| 616 | counts.nWalletMinVersion ? "present" : "none"); | ||
| 617 | strJson += strprintf(" \"keypool_entries\": %u,\n", counts.nPool); | ||
| 618 | strJson += strprintf(" \"wallet_transactions\": %u,\n", counts.nTransactions); | ||
| 619 | strJson += strprintf(" \"address_book_labels\": %u,\n", counts.nNames); | ||
| 620 | strJson += strprintf(" \"settings\": %u,\n", counts.nSettings); | ||
| 621 | strJson += strprintf(" \"hd_metadata_records\": %u,\n", | ||
| 622 | counts.nHDNext + counts.nHDSchema + counts.nHDCoinType + | ||
| 623 | counts.nHDReceiveNext + counts.nHDChangeNext); | ||
| 624 | strJson += strprintf(" \"database_version_records\": %u\n", counts.nVersion); | ||
| 625 | strJson += "}\n"; | ||
| 626 | return strJson; | ||
| 627 | } | ||
| 628 | |||
| 629 | static bool WriteAuditTextFile(const std::string& strPath, | ||
| 630 | const std::string& strText, | ||
| 631 | std::string& strError) | ||
| 632 | { | ||
| 633 | FILE* pf = fopen(strPath.c_str(), "wb"); | ||
| 634 | if (!pf) | ||
| 635 | { | ||
| 636 | strError = "cannot open output file"; | ||
| 637 | return false; | ||
| 638 | } | ||
| 639 | size_t nWritten = fwrite(strText.data(), 1, strText.size(), pf); | ||
| 640 | bool fCloseOk = fclose(pf) == 0; | ||
| 641 | if (nWritten != strText.size() || !fCloseOk) | ||
| 642 | { | ||
| 643 | strError = "could not write the complete output file"; | ||
| 644 | return false; | ||
| 645 | } | ||
| 646 | return true; | ||
| 647 | } | ||
| 648 | |||
| 649 | static void AddWalletStorageFailure(std::vector<std::string>& vFailures, | ||
| 650 | const std::string& strFailure) | ||
| 651 | { | ||
| 652 | vFailures.push_back(strFailure); | ||
| 653 | } | ||
| 654 | |||
| 655 | static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& counts, | ||
| 656 | std::vector<std::string>& vFailures) | ||
| 657 | { | ||
| 658 | bool fPlainHD = counts.nPlainHDMaster > 0 || counts.nPlainHDChainCode > 0; | ||
| 659 | bool fCryptedHD = counts.nCryptedHDMaster > 0 || counts.nCryptedHDChainCode > 0; | ||
| 660 | bool fEncryptedRecords = counts.nEncryptedKeys > 0 || | ||
| 661 | counts.nMasterKeys > 0 || | ||
| 662 | fCryptedHD; | ||
| 663 | |||
| 664 | if (counts.nMalformed > 0) | ||
| 665 | AddWalletStorageFailure(vFailures, "wallet.dat contains malformed records"); | ||
| 666 | if (counts.nUnknown > 0) | ||
| 667 | AddWalletStorageFailure(vFailures, "wallet.dat contains unknown records"); | ||
| 668 | if ((counts.nPlainHDMaster > 0) != (counts.nPlainHDChainCode > 0)) | ||
| 669 | AddWalletStorageFailure(vFailures, "plain HD seed is incomplete"); | ||
| 670 | if ((counts.nCryptedHDMaster > 0) != (counts.nCryptedHDChainCode > 0)) | ||
| 671 | AddWalletStorageFailure(vFailures, "encrypted HD seed is incomplete"); | ||
| 672 | if (fPlainHD && fCryptedHD) | ||
| 673 | AddWalletStorageFailure(vFailures, "wallet.dat contains both plain and encrypted HD seed records"); | ||
| 674 | if (counts.nDefaultKey > 1) | ||
| 675 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one default public key record"); | ||
| 676 | if (counts.nWalletMinVersion > 1) | ||
| 677 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one minimum-version record"); | ||
| 678 | |||
| 679 | if (fEncryptedRecords) | ||
| 680 | { | ||
| 681 | if (counts.nPlainKeys > 0) | ||
| 682 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain private key records"); | ||
| 683 | if (fPlainHD) | ||
| 684 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain HD seed records"); | ||
| 685 | if (counts.nMasterKeys == 0) | ||
| 686 | AddWalletStorageFailure(vFailures, "encrypted wallet has no encryption master key record"); | ||
| 687 | if (counts.nWalletMinVersion == 0) | ||
| 688 | AddWalletStorageFailure(vFailures, "encrypted wallet has no minimum-version record"); | ||
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | int CmdWalletStorageCheck() | ||
| 693 | { | ||
| 694 | AttachTerminal(); | ||
| 695 | |||
| 696 | WalletStorageAuditCounts counts; | ||
| 697 | std::string strError; | ||
| 698 | if (!ReadWalletStorageCounts(counts, strError)) | ||
| 699 | { | ||
| 700 | fprintf(stderr, "Cannot check wallet storage: %s\n", strError.c_str()); | ||
| 701 | return 1; | ||
| 702 | } | ||
| 703 | |||
| 704 | std::vector<std::string> vFailures; | ||
| 705 | BuildWalletStorageSanityFailures(counts, vFailures); | ||
| 706 | |||
| 707 | printf("Wallet storage sanity check\n"); | ||
| 708 | printf(" records total: %u\n", counts.nTotal); | ||
| 709 | printf(" encrypted records: %s\n", | ||
| 710 | (counts.nEncryptedKeys || counts.nMasterKeys || | ||
| 711 | counts.nCryptedHDMaster || counts.nCryptedHDChainCode) ? "yes" : "no"); | ||
| 712 | printf(" plain private keys: %u\n", counts.nPlainKeys); | ||
| 713 | printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); | ||
| 714 | printf(" plain HD seed: %s\n", | ||
| 715 | HDSeedStorageState(counts.nPlainHDMaster, | ||
| 716 | counts.nPlainHDChainCode).c_str()); | ||
| 717 | printf(" encrypted HD seed: %s\n", | ||
| 718 | HDSeedStorageState(counts.nCryptedHDMaster, | ||
| 719 | counts.nCryptedHDChainCode).c_str()); | ||
| 720 | if (vFailures.empty()) | ||
| 721 | { | ||
| 722 | printf(" storage sanity: ok\n"); | ||
| 723 | fflush(stdout); | ||
| 724 | return 0; | ||
| 725 | } | ||
| 726 | |||
| 727 | printf(" storage sanity: failed\n"); | ||
| 728 | for (std::vector<std::string>::const_iterator it = vFailures.begin(); | ||
| 729 | it != vFailures.end(); ++it) | ||
| 730 | printf(" failure: %s\n", it->c_str()); | ||
| 731 | fflush(stdout); | ||
| 732 | return 2; | ||
| 733 | } | ||
| 734 | |||
| 735 | int CmdWalletStorageAudit(const std::string& strJsonOut) | ||
| 736 | { | ||
| 737 | AttachTerminal(); | ||
| 738 | |||
| 739 | WalletStorageAuditCounts counts; | ||
| 740 | std::string strError; | ||
| 741 | if (!ReadWalletStorageCounts(counts, strError)) | ||
| 742 | { | ||
| 743 | fprintf(stderr, "Cannot audit wallet storage: %s\n", strError.c_str()); | ||
| 744 | return 1; | ||
| 745 | } | ||
| 746 | |||
| 747 | if (!strJsonOut.empty()) | ||
| 748 | { | ||
| 749 | std::string strWriteError; | ||
| 750 | if (!WriteAuditTextFile(strJsonOut, | ||
| 751 | WalletStorageAuditJson(counts), | ||
| 752 | strWriteError)) | ||
| 753 | { | ||
| 754 | fprintf(stderr, "Cannot write wallet storage audit JSON: %s\n", | ||
| 755 | strWriteError.c_str()); | ||
| 756 | return 1; | ||
| 757 | } | ||
| 758 | printf("Wallet storage audit written to %s\n", strJsonOut.c_str()); | ||
| 759 | fflush(stdout); | ||
| 760 | return counts.nMalformed > 0 ? 2 : 0; | ||
| 761 | } | ||
| 762 | |||
| 763 | printf("Wallet storage audit\n"); | ||
| 764 | printf(" records total: %u\n", counts.nTotal); | ||
| 765 | printf(" malformed records: %u\n", counts.nMalformed); | ||
| 766 | printf(" unknown records: %u\n", counts.nUnknown); | ||
| 767 | printf(" plain private keys: %u\n", counts.nPlainKeys); | ||
| 768 | printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); | ||
| 769 | printf(" encryption master keys: %u\n", counts.nMasterKeys); | ||
| 770 | printf(" default public key: %s\n", counts.nDefaultKey ? "present" : "none"); | ||
| 771 | printf(" plain HD seed: %s\n", | ||
| 772 | HDSeedStorageState(counts.nPlainHDMaster, | ||
| 773 | counts.nPlainHDChainCode).c_str()); | ||
| 774 | printf(" encrypted HD seed: %s\n", | ||
| 775 | HDSeedStorageState(counts.nCryptedHDMaster, | ||
| 776 | counts.nCryptedHDChainCode).c_str()); | ||
| 777 | printf(" wallet minimum version: %s\n", | ||
| 778 | counts.nWalletMinVersion ? "present" : "none"); | ||
| 779 | printf(" keypool entries: %u\n", counts.nPool); | ||
| 780 | printf(" wallet transactions: %u\n", counts.nTransactions); | ||
| 781 | printf(" address book labels: %u\n", counts.nNames); | ||
| 782 | printf(" settings: %u\n", counts.nSettings); | ||
| 783 | printf(" HD metadata records: %u\n", | ||
| 784 | counts.nHDNext + counts.nHDSchema + counts.nHDCoinType + | ||
| 785 | counts.nHDReceiveNext + counts.nHDChangeNext); | ||
| 786 | printf(" database version records: %u\n", counts.nVersion); | ||
| 787 | fflush(stdout); | ||
| 788 | |||
| 789 | if (counts.nMalformed > 0) | ||
| 790 | return 2; | ||
| 791 | return 0; | ||
| 792 | } | ||
| 793 | |||
| 794 | static vector<unsigned char> DataStreamBytesLocal(const CDataStream& ss) | ||
| 795 | { | ||
| 796 | return vector<unsigned char>(ss.begin(), ss.end()); | ||
| 797 | } | ||
| 798 | |||
| 799 | namespace { | ||
| 800 | |||
| 801 | static void RemoveSQLiteExportFiles(const string& strPath) | ||
| 802 | { | ||
| 803 | remove(strPath.c_str()); | ||
| 804 | remove((strPath + "-wal").c_str()); | ||
| 805 | remove((strPath + "-shm").c_str()); | ||
| 806 | } | ||
| 807 | |||
| 808 | static void RemoveFailedRestoreWallet(const string& strWalletPath) | ||
| 809 | { | ||
| 810 | DBFlush(true); | ||
| 811 | remove(strWalletPath.c_str()); | ||
| 812 | } | ||
| 813 | |||
| 814 | class CWalletSQLiteExportVisitor : public CWalletRecordVisitor | ||
| 815 | { | ||
| 816 | public: | ||
| 817 | CWalletDBSQLite& db; | ||
| 818 | int& nCopied; | ||
| 819 | |||
| 820 | CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn) | ||
| 821 | : db(dbIn), nCopied(nCopiedIn) { } | ||
| 822 | |||
| 823 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 824 | const CDataStream& ssValue, | ||
| 825 | string& strErrorRet) | ||
| 826 | { | ||
| 827 | if (!db.WriteRecord(DataStreamBytesLocal(ssKey), | ||
| 828 | DataStreamBytesLocal(ssValue), | ||
| 829 | strErrorRet)) | ||
| 830 | return false; | ||
| 831 | nCopied++; | ||
| 832 | return true; | ||
| 833 | } | ||
| 834 | }; | ||
| 835 | |||
| 836 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 837 | { | ||
| 838 | public: | ||
| 839 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords; | ||
| 840 | |||
| 841 | explicit CWalletRecordMapVisitor( | ||
| 842 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn) | ||
| 843 | : mapRecords(mapRecordsIn) { } | ||
| 844 | |||
| 845 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 846 | const CDataStream& ssValue, | ||
| 847 | string& strErrorRet) | ||
| 848 | { | ||
| 849 | vector<unsigned char> vchKey = DataStreamBytesLocal(ssKey); | ||
| 850 | if (mapRecords.count(vchKey)) | ||
| 851 | { | ||
| 852 | strErrorRet = "duplicate serialized wallet key"; | ||
| 853 | return false; | ||
| 854 | } | ||
| 855 | mapRecords[vchKey] = DataStreamBytesLocal(ssValue); | ||
| 856 | return true; | ||
| 857 | } | ||
| 858 | }; | ||
| 859 | |||
| 860 | class CWalletRawRestoreDB : public CWalletDB | ||
| 861 | { | ||
| 862 | public: | ||
| 863 | CWalletRawRestoreDB(const char* pszMode="cr+", bool fTxn=true) | ||
| 864 | : CWalletDB(pszMode, fTxn) { } | ||
| 865 | |||
| 866 | bool WriteRaw(CDataStream ssKey, CDataStream ssValue) | ||
| 867 | { | ||
| 868 | if (!pdb || ssKey.empty()) | ||
| 869 | return false; | ||
| 870 | |||
| 871 | Dbt datKey((void*)&ssKey[0], ssKey.size()); | ||
| 872 | Dbt datValue(ssValue.empty() ? NULL : (void*)&ssValue[0], | ||
| 873 | ssValue.size()); | ||
| 874 | int ret = pdb->put(GetTxn(), &datKey, &datValue, 0); | ||
| 875 | |||
| 876 | memset(datKey.get_data(), 0, datKey.get_size()); | ||
| 877 | if (datValue.get_data()) | ||
| 878 | memset(datValue.get_data(), 0, datValue.get_size()); | ||
| 879 | return ret == 0; | ||
| 880 | } | ||
| 881 | }; | ||
| 882 | |||
| 883 | class CWalletSQLiteRestoreVisitor : public CWalletRecordVisitor | ||
| 884 | { | ||
| 885 | public: | ||
| 886 | CWalletRawRestoreDB& db; | ||
| 887 | int& nCopied; | ||
| 888 | |||
| 889 | CWalletSQLiteRestoreVisitor(CWalletRawRestoreDB& dbIn, int& nCopiedIn) | ||
| 890 | : db(dbIn), nCopied(nCopiedIn) { } | ||
| 891 | |||
| 892 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 893 | const CDataStream& ssValue, | ||
| 894 | string& strErrorRet) | ||
| 895 | { | ||
| 896 | if (!db.WriteRaw(ssKey, ssValue)) | ||
| 897 | { | ||
| 898 | strErrorRet = "could not write raw wallet record"; | ||
| 899 | return false; | ||
| 900 | } | ||
| 901 | nCopied++; | ||
| 902 | return true; | ||
| 903 | } | ||
| 904 | }; | ||
| 905 | |||
| 906 | class CWalletSQLiteLoadCheckVisitor : public CWalletRecordVisitor | ||
| 907 | { | ||
| 908 | public: | ||
| 909 | int nRecords; | ||
| 910 | int nUnknown; | ||
| 911 | string strFailure; | ||
| 912 | |||
| 913 | CWalletSQLiteLoadCheckVisitor() : nRecords(0), nUnknown(0) { } | ||
| 914 | |||
| 915 | bool Fail(const string& strMessage, string& strErrorRet) | ||
| 916 | { | ||
| 917 | strFailure = strMessage; | ||
| 918 | strErrorRet = strMessage; | ||
| 919 | return false; | ||
| 920 | } | ||
| 921 | |||
| 922 | bool VisitWalletRecord(const CDataStream& ssKeyIn, | ||
| 923 | const CDataStream& ssValueIn, | ||
| 924 | string& strErrorRet) | ||
| 925 | { | ||
| 926 | nRecords++; | ||
| 927 | try | ||
| 928 | { | ||
| 929 | CDataStream ssKey = ssKeyIn; | ||
| 930 | CDataStream ssValue = ssValueIn; | ||
| 931 | string strType; | ||
| 932 | ssKey >> strType; | ||
| 933 | if (ssKey.fail()) | ||
| 934 | return Fail("malformed wallet record key", strErrorRet); | ||
| 935 | |||
| 936 | if (strType == "name") | ||
| 937 | { | ||
| 938 | string strAddress; | ||
| 939 | string strName; | ||
| 940 | ssKey >> strAddress; | ||
| 941 | ssValue >> strName; | ||
| 942 | } | ||
| 943 | else if (strType == "tx") | ||
| 944 | { | ||
| 945 | uint256 hash; | ||
| 946 | CWalletTx wtx; | ||
| 947 | ssKey >> hash; | ||
| 948 | ssValue >> wtx; | ||
| 949 | } | ||
| 950 | else if (strType == "key") | ||
| 951 | { | ||
| 952 | vector<unsigned char> vchPubKey; | ||
| 953 | CPrivKey vchPrivKey; | ||
| 954 | ssKey >> vchPubKey; | ||
| 955 | ssValue >> vchPrivKey; | ||
| 956 | } | ||
| 957 | else if (strType == "mkey") | ||
| 958 | { | ||
| 959 | unsigned int nID = 0; | ||
| 960 | CWalletMasterKey kMasterKey; | ||
| 961 | ssKey >> nID; | ||
| 962 | ssValue >> kMasterKey; | ||
| 963 | } | ||
| 964 | else if (strType == "ckey") | ||
| 965 | { | ||
| 966 | vector<unsigned char> vchPubKey; | ||
| 967 | vector<unsigned char> vchCryptedSecret; | ||
| 968 | ssKey >> vchPubKey; | ||
| 969 | ssValue >> vchCryptedSecret; | ||
| 970 | } | ||
| 971 | else if (strType == "defaultkey" || | ||
| 972 | strType == "hdmaster" || | ||
| 973 | strType == "hdchaincode" || | ||
| 974 | strType == "cryptedhdmaster" || | ||
| 975 | strType == "cryptedhdchaincode") | ||
| 976 | { | ||
| 977 | vector<unsigned char> vch; | ||
| 978 | ssValue >> vch; | ||
| 979 | } | ||
| 980 | else if (strType == "hdnext" || | ||
| 981 | strType == "hdreceivenext" || | ||
| 982 | strType == "hdchangenext" || | ||
| 983 | strType == "hdcointype") | ||
| 984 | { | ||
| 985 | unsigned int nValue = 0; | ||
| 986 | ssValue >> nValue; | ||
| 987 | } | ||
| 988 | else if (strType == "hdschema") | ||
| 989 | { | ||
| 990 | int nSchema = 0; | ||
| 991 | ssValue >> nSchema; | ||
| 992 | if (!ssValue.fail() && | ||
| 993 | nSchema != HD_SCHEMA_NONE && | ||
| 994 | nSchema != HD_SCHEMA_LEGACY && | ||
| 995 | nSchema != HD_SCHEMA_BIP44) | ||
| 996 | return Fail("unsupported deterministic wallet schema", | ||
| 997 | strErrorRet); | ||
| 998 | } | ||
| 999 | else if (strType == "walletminversion") | ||
| 1000 | { | ||
| 1001 | int nMinVersion = 0; | ||
| 1002 | ssValue >> nMinVersion; | ||
| 1003 | if (!ssValue.fail() && nMinVersion > WALLET_FORMAT_SUPPORTED) | ||
| 1004 | return Fail("unsupported future wallet format", strErrorRet); | ||
| 1005 | } | ||
| 1006 | else if (strType == "pool") | ||
| 1007 | { | ||
| 1008 | int64 nIndex = 0; | ||
| 1009 | vector<unsigned char> vchPubKey; | ||
| 1010 | ssKey >> nIndex; | ||
| 1011 | ssValue >> vchPubKey; | ||
| 1012 | } | ||
| 1013 | else if (strType == "setting") | ||
| 1014 | { | ||
| 1015 | string strKey; | ||
| 1016 | ssKey >> strKey; | ||
| 1017 | if (strKey == "nTransactionFee") | ||
| 1018 | { | ||
| 1019 | int64 nValue = 0; | ||
| 1020 | ssValue >> nValue; | ||
| 1021 | } | ||
| 1022 | else if (strKey == "addrIncoming") | ||
| 1023 | { | ||
| 1024 | CAddress addr; | ||
| 1025 | ssValue >> addr; | ||
| 1026 | } | ||
| 1027 | else if (strKey == "nMineMode") | ||
| 1028 | { | ||
| 1029 | int nValue = 0; | ||
| 1030 | ssValue >> nValue; | ||
| 1031 | } | ||
| 1032 | else if (strKey == "fGenerateBitcoins") | ||
| 1033 | { | ||
| 1034 | int fValue = 0; | ||
| 1035 | ssValue >> fValue; | ||
| 1036 | } | ||
| 1037 | else if (strKey == "strParticipantPool") | ||
| 1038 | { | ||
| 1039 | string strValue; | ||
| 1040 | ssValue >> strValue; | ||
| 1041 | } | ||
| 1042 | } | ||
| 1043 | else if (strType == "version") | ||
| 1044 | { | ||
| 1045 | int nVersion = 0; | ||
| 1046 | ssValue >> nVersion; | ||
| 1047 | } | ||
| 1048 | else | ||
| 1049 | { | ||
| 1050 | nUnknown++; | ||
| 1051 | } | ||
| 1052 | |||
| 1053 | if (ssKey.fail()) | ||
| 1054 | return Fail("malformed wallet record key", strErrorRet); | ||
| 1055 | if (ssValue.fail()) | ||
| 1056 | return Fail("malformed wallet record value", strErrorRet); | ||
| 1057 | } | ||
| 1058 | catch (const std::exception& e) | ||
| 1059 | { | ||
| 1060 | return Fail(strprintf("wallet record parse exception: %s", e.what()), | ||
| 1061 | strErrorRet); | ||
| 1062 | } | ||
| 1063 | catch (...) | ||
| 1064 | { | ||
| 1065 | return Fail("wallet record parse exception", strErrorRet); | ||
| 1066 | } | ||
| 1067 | return true; | ||
| 1068 | } | ||
| 1069 | }; | ||
| 1070 | |||
| 1071 | } // namespace | ||
| 1072 | |||
| 1073 | // Export the currently-loaded wallet to a SQLite file. Shared by the | ||
| 1074 | // -walletsqliteexport command and the GUI conversion wizard: opens the | ||
| 1075 | // destination, copies every record inside one transaction, checkpoints, and | ||
| 1076 | // verifies the stored count matches. Returns false with strErrorRet set on any | ||
| 1077 | // failure and leaves no partial file behind; the source wallet is only read. | ||
| 1078 | bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, | ||
| 1079 | std::string& strErrorRet) | ||
| 1080 | { | ||
| 1081 | nCopiedRet = 0; | ||
| 1082 | if (strDest.empty()) | ||
| 1083 | { | ||
| 1084 | strErrorRet = "no SQLite export path was given"; | ||
| 1085 | return false; | ||
| 1086 | } | ||
| 1087 | if (FileExists(strDest.c_str())) | ||
| 1088 | { | ||
| 1089 | strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str()); | ||
| 1090 | return false; | ||
| 1091 | } | ||
| 1092 | |||
| 1093 | CWalletDBSQLite db; | ||
| 1094 | if (!db.Open(strDest, strErrorRet)) | ||
| 1095 | { | ||
| 1096 | RemoveSQLiteExportFiles(strDest); | ||
| 1097 | return false; | ||
| 1098 | } | ||
| 1099 | if (!db.BeginTransaction(strErrorRet)) | ||
| 1100 | { | ||
| 1101 | db.Close(); | ||
| 1102 | RemoveSQLiteExportFiles(strDest); | ||
| 1103 | return false; | ||
| 1104 | } | ||
| 1105 | |||
| 1106 | CWalletSQLiteExportVisitor visitor(db, nCopiedRet); | ||
| 1107 | if (!ScanWalletRecords(visitor, strErrorRet)) | ||
| 1108 | { | ||
| 1109 | string strRollbackError; | ||
| 1110 | db.RollbackTransaction(strRollbackError); | ||
| 1111 | db.Close(); | ||
| 1112 | RemoveSQLiteExportFiles(strDest); | ||
| 1113 | return false; | ||
| 1114 | } | ||
| 1115 | if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet)) | ||
| 1116 | { | ||
| 1117 | db.Close(); | ||
| 1118 | RemoveSQLiteExportFiles(strDest); | ||
| 1119 | return false; | ||
| 1120 | } | ||
| 1121 | |||
| 1122 | int nCount = 0; | ||
| 1123 | if (!db.CountRecords(nCount, strErrorRet)) | ||
| 1124 | { | ||
| 1125 | db.Close(); | ||
| 1126 | RemoveSQLiteExportFiles(strDest); | ||
| 1127 | return false; | ||
| 1128 | } | ||
| 1129 | if (nCount != nCopiedRet) | ||
| 1130 | { | ||
| 1131 | strErrorRet = strprintf("export count mismatch: copied %d, stored %d", | ||
| 1132 | nCopiedRet, nCount); | ||
| 1133 | db.Close(); | ||
| 1134 | RemoveSQLiteExportFiles(strDest); | ||
| 1135 | return false; | ||
| 1136 | } | ||
| 1137 | db.Close(); | ||
| 1138 | return true; | ||
| 1139 | } | ||
| 1140 | |||
| 1141 | // Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export | ||
| 1142 | // wallet.dat to <datadir>/wallet.sqlite, then record the backend marker so the | ||
| 1143 | // next start opens SQLite. wallet.dat is never modified -- it stays as the | ||
| 1144 | // fallback. Refuses if already on SQLite or if a wallet.sqlite already exists. | ||
| 1145 | bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet) | ||
| 1146 | { | ||
| 1147 | nCopiedRet = 0; | ||
| 1148 | if (WalletSQLiteRuntimeActive()) | ||
| 1149 | { | ||
| 1150 | strErrorRet = "this wallet is already on the SQLite backend"; | ||
| 1151 | return false; | ||
| 1152 | } | ||
| 1153 | string strDest = GetAppDir() + "/wallet.sqlite"; | ||
| 1154 | if (FileExists(strDest.c_str())) | ||
| 1155 | { | ||
| 1156 | strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str()); | ||
| 1157 | return false; | ||
| 1158 | } | ||
| 1159 | if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet)) | ||
| 1160 | return false; | ||
| 1161 | if (!WriteWalletBackendMarker("sqlite")) | ||
| 1162 | { | ||
| 1163 | strErrorRet = "the wallet was exported but the backend choice could not be saved"; | ||
| 1164 | return false; | ||
| 1165 | } | ||
| 1166 | return true; | ||
| 1167 | } | ||
| 1168 | |||
| 1169 | int CmdWalletSQLiteExport(const std::string& strDest) | ||
| 1170 | { | ||
| 1171 | AttachTerminal(); | ||
| 1172 | |||
| 1173 | // Keep the command's own specific messages for the two conditions the | ||
| 1174 | // shared helper folds into a generic error, so scripts and the tests that | ||
| 1175 | // read this output see the same wording as before. | ||
| 1176 | if (strDest.empty()) | ||
| 1177 | { | ||
| 1178 | fprintf(stderr, "Missing SQLite wallet export path.\n"); | ||
| 1179 | return 1; | ||
| 1180 | } | ||
| 1181 | if (FileExists(strDest.c_str())) | ||
| 1182 | { | ||
| 1183 | fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", | ||
| 1184 | strDest.c_str()); | ||
| 1185 | return 1; | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | int nCopied = 0; | ||
| 1189 | string strError; | ||
| 1190 | if (!ExportActiveWalletToSQLite(strDest, nCopied, strError)) | ||
| 1191 | { | ||
| 1192 | fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str()); | ||
| 1193 | return 1; | ||
| 1194 | } | ||
| 1195 | |||
| 1196 | printf("SQLite wallet export written to %s\n", strDest.c_str()); | ||
| 1197 | printf(" records copied: %d\n", nCopied); | ||
| 1198 | fflush(stdout); | ||
| 1199 | return 0; | ||
| 1200 | } | ||
| 1201 | |||
| 1202 | int CmdWalletSQLiteVerify(const std::string& strPath) | ||
| 1203 | { | ||
| 1204 | AttachTerminal(); | ||
| 1205 | |||
| 1206 | if (strPath.empty()) | ||
| 1207 | { | ||
| 1208 | fprintf(stderr, "Missing SQLite wallet export path.\n"); | ||
| 1209 | return 1; | ||
| 1210 | } | ||
| 1211 | if (!FileExists(strPath.c_str())) | ||
| 1212 | { | ||
| 1213 | fprintf(stderr, "SQLite wallet export does not exist: %s\n", | ||
| 1214 | strPath.c_str()); | ||
| 1215 | return 1; | ||
| 1216 | } | ||
| 1217 | |||
| 1218 | std::map<vector<unsigned char>, vector<unsigned char> > mapBDB; | ||
| 1219 | std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite; | ||
| 1220 | string strError; | ||
| 1221 | |||
| 1222 | CWalletRecordMapVisitor bdbVisitor(mapBDB); | ||
| 1223 | if (!ScanWalletRecords(bdbVisitor, strError)) | ||
| 1224 | { | ||
| 1225 | fprintf(stderr, "Cannot scan wallet.dat records: %s\n", strError.c_str()); | ||
| 1226 | return 1; | ||
| 1227 | } | ||
| 1228 | |||
| 1229 | CWalletDBSQLite db; | ||
| 1230 | if (!db.Open(strPath, strError)) | ||
| 1231 | { | ||
| 1232 | fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); | ||
| 1233 | return 1; | ||
| 1234 | } | ||
| 1235 | CWalletRecordMapVisitor sqliteVisitor(mapSQLite); | ||
| 1236 | if (!db.ScanRecords(sqliteVisitor, strError)) | ||
| 1237 | { | ||
| 1238 | fprintf(stderr, "Cannot scan SQLite wallet export: %s\n", strError.c_str()); | ||
| 1239 | return 1; | ||
| 1240 | } | ||
| 1241 | |||
| 1242 | int nMissing = 0; | ||
| 1243 | int nMismatched = 0; | ||
| 1244 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 1245 | mapBDB.begin(); it != mapBDB.end(); ++it) | ||
| 1246 | { | ||
| 1247 | std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator sit = | ||
| 1248 | mapSQLite.find(it->first); | ||
| 1249 | if (sit == mapSQLite.end()) | ||
| 1250 | nMissing++; | ||
| 1251 | else if (sit->second != it->second) | ||
| 1252 | nMismatched++; | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | int nExtra = 0; | ||
| 1256 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 1257 | mapSQLite.begin(); it != mapSQLite.end(); ++it) | ||
| 1258 | { | ||
| 1259 | if (!mapBDB.count(it->first)) | ||
| 1260 | nExtra++; | ||
| 1261 | } | ||
| 1262 | |||
| 1263 | printf("SQLite wallet export verification\n"); | ||
| 1264 | printf(" wallet.dat records: %u\n", (unsigned int)mapBDB.size()); | ||
| 1265 | printf(" SQLite records: %u\n", (unsigned int)mapSQLite.size()); | ||
| 1266 | printf(" missing records: %d\n", nMissing); | ||
| 1267 | printf(" extra records: %d\n", nExtra); | ||
| 1268 | printf(" record value mismatches: %d\n", nMismatched); | ||
| 1269 | if (nMissing == 0 && nExtra == 0 && nMismatched == 0) | ||
| 1270 | { | ||
| 1271 | printf(" verification: ok\n"); | ||
| 1272 | fflush(stdout); | ||
| 1273 | return 0; | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | printf(" verification: failed\n"); | ||
| 1277 | fflush(stdout); | ||
| 1278 | return 2; | ||
| 1279 | } | ||
| 1280 | |||
| 1281 | int CmdWalletSQLiteRestore(const std::string& strPath) | ||
| 1282 | { | ||
| 1283 | AttachTerminal(); | ||
| 1284 | |||
| 1285 | if (strPath.empty()) | ||
| 1286 | { | ||
| 1287 | fprintf(stderr, "Missing SQLite wallet export path.\n"); | ||
| 1288 | return 1; | ||
| 1289 | } | ||
| 1290 | if (!FileExists(strPath.c_str())) | ||
| 1291 | { | ||
| 1292 | fprintf(stderr, "SQLite wallet export does not exist: %s\n", | ||
| 1293 | strPath.c_str()); | ||
| 1294 | return 1; | ||
| 1295 | } | ||
| 1296 | |||
| 1297 | string strWalletPath = GetAppDir() + "/wallet.dat"; | ||
| 1298 | if (FileExists(strWalletPath.c_str())) | ||
| 1299 | { | ||
| 1300 | fprintf(stderr, "Refusing to overwrite existing wallet.dat: %s\n", | ||
| 1301 | strWalletPath.c_str()); | ||
| 1302 | return 1; | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | string strError; | ||
| 1306 | CWalletDBSQLite sqlite; | ||
| 1307 | if (!sqlite.Open(strPath, strError)) | ||
| 1308 | { | ||
| 1309 | fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); | ||
| 1310 | return 1; | ||
| 1311 | } | ||
| 1312 | |||
| 1313 | int nSQLiteRecords = 0; | ||
| 1314 | if (!sqlite.CountRecords(nSQLiteRecords, strError)) | ||
| 1315 | { | ||
| 1316 | fprintf(stderr, "Cannot count SQLite wallet export records: %s\n", | ||
| 1317 | strError.c_str()); | ||
| 1318 | return 1; | ||
| 1319 | } | ||
| 1320 | if (nSQLiteRecords <= 0) | ||
| 1321 | { | ||
| 1322 | fprintf(stderr, "SQLite wallet export contains no records.\n"); | ||
| 1323 | return 2; | ||
| 1324 | } | ||
| 1325 | |||
| 1326 | CWalletRawRestoreDB bdb("cr+", true); | ||
| 1327 | if (!bdb.TxnBegin()) | ||
| 1328 | { | ||
| 1329 | fprintf(stderr, "Cannot begin Berkeley DB restore transaction.\n"); | ||
| 1330 | return 1; | ||
| 1331 | } | ||
| 1332 | |||
| 1333 | int nCopied = 0; | ||
| 1334 | CWalletSQLiteRestoreVisitor visitor(bdb, nCopied); | ||
| 1335 | if (!sqlite.ScanRecords(visitor, strError)) | ||
| 1336 | { | ||
| 1337 | bdb.TxnAbort(); | ||
| 1338 | bdb.Close(); | ||
| 1339 | RemoveFailedRestoreWallet(strWalletPath); | ||
| 1340 | fprintf(stderr, "Cannot restore SQLite wallet export: %s\n", | ||
| 1341 | strError.c_str()); | ||
| 1342 | return 1; | ||
| 1343 | } | ||
| 1344 | if (nCopied != nSQLiteRecords) | ||
| 1345 | { | ||
| 1346 | bdb.TxnAbort(); | ||
| 1347 | bdb.Close(); | ||
| 1348 | RemoveFailedRestoreWallet(strWalletPath); | ||
| 1349 | fprintf(stderr, "SQLite wallet restore copied %d records, expected %d.\n", | ||
| 1350 | nCopied, nSQLiteRecords); | ||
| 1351 | return 2; | ||
| 1352 | } | ||
| 1353 | if (!bdb.TxnCommit()) | ||
| 1354 | { | ||
| 1355 | bdb.Close(); | ||
| 1356 | RemoveFailedRestoreWallet(strWalletPath); | ||
| 1357 | fprintf(stderr, "Cannot commit Berkeley DB restore transaction.\n"); | ||
| 1358 | return 1; | ||
| 1359 | } | ||
| 1360 | |||
| 1361 | printf("SQLite wallet export restored to wallet.dat\n"); | ||
| 1362 | printf(" records restored: %d\n", nCopied); | ||
| 1363 | fflush(stdout); | ||
| 1364 | return 0; | ||
| 1365 | } | ||
| 1366 | |||
| 1367 | int CmdWalletSQLiteLoadCheck(const std::string& strPath) | ||
| 1368 | { | ||
| 1369 | AttachTerminal(); | ||
| 1370 | |||
| 1371 | if (strPath.empty()) | ||
| 1372 | { | ||
| 1373 | fprintf(stderr, "Missing SQLite wallet export path.\n"); | ||
| 1374 | return 1; | ||
| 1375 | } | ||
| 1376 | if (!FileExists(strPath.c_str())) | ||
| 1377 | { | ||
| 1378 | fprintf(stderr, "SQLite wallet export does not exist: %s\n", | ||
| 1379 | strPath.c_str()); | ||
| 1380 | return 1; | ||
| 1381 | } | ||
| 1382 | |||
| 1383 | string strError; | ||
| 1384 | CWalletDBSQLite sqlite; | ||
| 1385 | if (!sqlite.Open(strPath, strError)) | ||
| 1386 | { | ||
| 1387 | fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); | ||
| 1388 | return 1; | ||
| 1389 | } | ||
| 1390 | |||
| 1391 | CWalletSQLiteLoadCheckVisitor visitor; | ||
| 1392 | if (!sqlite.ScanRecords(visitor, strError)) | ||
| 1393 | { | ||
| 1394 | printf("SQLite wallet load check\n"); | ||
| 1395 | printf(" records checked: %d\n", visitor.nRecords); | ||
| 1396 | printf(" unknown records: %d\n", visitor.nUnknown); | ||
| 1397 | printf(" load check: failed\n"); | ||
| 1398 | printf(" failure: %s\n", strError.c_str()); | ||
| 1399 | fflush(stdout); | ||
| 1400 | return 2; | ||
| 1401 | } | ||
| 1402 | |||
| 1403 | printf("SQLite wallet load check\n"); | ||
| 1404 | printf(" records checked: %d\n", visitor.nRecords); | ||
| 1405 | printf(" unknown records: %d\n", visitor.nUnknown); | ||
| 1406 | printf(" load check: ok\n"); | ||
| 1407 | fflush(stdout); | ||
| 1408 | return 0; | ||
| 1409 | } | ||
| 1410 | |||
| 459 | int CmdEncryptWallet(const std::string& strPassphrase) | 1411 | int CmdEncryptWallet(const std::string& strPassphrase) |
| 460 | { | 1412 | { |
| 461 | AttachTerminal(); | 1413 | AttachTerminal(); |
| 462 | 1414 | ||
| 1415 | // Capture the backend now: under SQLite, EncryptWallet() closes the runtime | ||
| 1416 | // store to swap the file, so WalletSQLiteRuntimeActive() reads false by the | ||
| 1417 | // time we would decide whether to purge Berkeley DB logs below. | ||
| 1418 | bool fSQLiteBackend = WalletSQLiteRuntimeActive(); | ||
| 1419 | |||
| 463 | string strBackup; | 1420 | string strBackup; |
| 464 | string strError; | 1421 | string strError; |
| 465 | if (!EncryptWallet(strPassphrase, strBackup, strError)) | 1422 | if (!EncryptWallet(strPassphrase, strBackup, strError)) |
| @@ -470,6 +1427,8 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 470 | } | 1427 | } |
| 471 | 1428 | ||
| 472 | printf("Wallet encrypted.\n"); | 1429 | printf("Wallet encrypted.\n"); |
| 1430 | if (fSQLiteBackend) | ||
| 1431 | printf("Wallet backend: SQLite (wallet.sqlite).\n"); | ||
| 473 | printf("The old unencrypted wallet was moved to:\n"); | 1432 | printf("The old unencrypted wallet was moved to:\n"); |
| 474 | printf(" %s\n", strBackup.c_str()); | 1433 | printf(" %s\n", strBackup.c_str()); |
| 475 | printf("That backup still contains private keys in plain text. Move it offline or\n"); | 1434 | printf("That backup still contains private keys in plain text. Move it offline or\n"); |
| @@ -482,7 +1441,13 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 482 | // them. Closing the environment and dropping the logs is the last step of | 1441 | // them. Closing the environment and dropping the logs is the last step of |
| 483 | // encrypting, not an optimisation -- our own backup advice is to copy the | 1442 | // encrypting, not an optimisation -- our own backup advice is to copy the |
| 484 | // whole directory, and following it otherwise carries the keys along. | 1443 | // whole directory, and following it otherwise carries the keys along. |
| 485 | PurgeDbEnvironmentLogs(); | 1444 | // |
| 1445 | // The SQLite backend has no Berkeley DB environment behind the wallet, and | ||
| 1446 | // EncryptWallet() already closed and swapped the SQLite store; there is | ||
| 1447 | // nothing to purge, and calling into the environment here would only open | ||
| 1448 | // one needlessly. | ||
| 1449 | if (!fSQLiteBackend) | ||
| 1450 | PurgeDbEnvironmentLogs(); | ||
| 486 | return 0; | 1451 | return 0; |
| 487 | } | 1452 | } |
| 488 | 1453 | ||
diff --git a/src/walletcmd.h b/src/walletcmd.h index 007b8b5..7013d44 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h | |||
| @@ -62,6 +62,40 @@ int CmdShowDerived(int nCount); | |||
| 62 | // by the installed recovery phrase or still depends on wallet.dat-only keys. | 62 | // by the installed recovery phrase or still depends on wallet.dat-only keys. |
| 63 | int CmdRecoveryAudit(); | 63 | int CmdRecoveryAudit(); |
| 64 | 64 | ||
| 65 | // Diagnostic: count wallet.dat record types without printing keys, | ||
| 66 | // addresses, transaction ids, labels, or other wallet values. | ||
| 67 | int CmdWalletStorageAudit(const std::string& strJsonOut = ""); | ||
| 68 | |||
| 69 | // Diagnostic: fail closed when wallet.dat contains an internally inconsistent | ||
| 70 | // or migration-unsafe mix of storage records. | ||
| 71 | int CmdWalletStorageCheck(); | ||
| 72 | |||
| 73 | // Copy every raw wallet.dat record into a SQLite key/value store, preserving | ||
| 74 | // serialized bytes. A migration/scripting tool; it does not change which backend | ||
| 75 | // the runtime wallet opens. | ||
| 76 | int CmdWalletSQLiteExport(const std::string& strDest); | ||
| 77 | |||
| 78 | // Export the currently-loaded wallet to a SQLite file (shared by the command | ||
| 79 | // above and the GUI wizard). Returns false with an error on any failure. | ||
| 80 | bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, | ||
| 81 | std::string& strErrorRet); | ||
| 82 | |||
| 83 | // Convert the running Berkeley DB wallet to SQLite and record the backend | ||
| 84 | // marker so the next start opens it. wallet.dat is left untouched as fallback. | ||
| 85 | bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet); | ||
| 86 | |||
| 87 | // Diagnostic/migration staging: compare a SQLite wallet export with the current | ||
| 88 | // wallet.dat raw records. Prints counts only, never record keys or values. | ||
| 89 | int CmdWalletSQLiteVerify(const std::string& strPath); | ||
| 90 | |||
| 91 | // Diagnostic/migration staging: rebuild wallet.dat from a SQLite export in an | ||
| 92 | // empty data directory. Refuses to overwrite an existing wallet.dat. | ||
| 93 | int CmdWalletSQLiteRestore(const std::string& strPath); | ||
| 94 | |||
| 95 | // Diagnostic/migration staging: parse a SQLite wallet export as the runtime | ||
| 96 | // wallet loader would. Prints counts only, never record keys or values. | ||
| 97 | int CmdWalletSQLiteLoadCheck(const std::string& strPath); | ||
| 98 | |||
| 65 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the | 99 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the |
| 66 | // given passphrase. Returns 0 on success and exits without starting the node. | 100 | // given passphrase. Returns 0 on success and exits without starting the node. |
| 67 | int CmdEncryptWallet(const std::string& strPassphrase); | 101 | int CmdEncryptWallet(const std::string& strPassphrase); |