The low-priority WalletMigration
benchmark existed for some time but was never run automatically in our CI.
Although the failure first surfaced on Windows as a hang during temporary directory cleanup, it could also be reproduced on Linux and macOS when forcing multiple iterations (e.g. via a long --min-time
).
Root causes
- Leaked open wallets on Windows
MigrateLegacyToDescriptor
produces two new descriptor wallets (the primary spendable wallet and a companion watch‑only wallet). Without unloading them, their database files remained open in theWalletContext
, blocking directory removal and hanging the test harness.
0what(): filesystem error: cannot remove all: The process cannot access the file because it is being used by another process [C:\Users\RUNNER\~1\AppData\Local\Temp\test_common bitcoin\WalletMigration\d8ffd89a7700ce01c31f] [C:\Users\RUNNER~1\AppData\Local\Temp\test_common bitcoin\WalletMigration\d8ffd89a7700ce01c31f\regtest\wallet.dat]
- Undefined behavior on repeated runs
The benchmark body calls
std::move(wallet)
, invalidating the localwallet
pointer. Running more than one iteration causes a use-after-move by the sanitizers.
0error: bench_bitcoin 0x00067927: DW_TAG_member '_M_local_buf' refers to type 0x00000000000b3ba7 which extends beyond the bounds of 0x0006791d
1* thread [#1](/bitcoin-bitcoin/1/), name = 'b-test', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0xc8)
2 * frame [#0](/bitcoin-bitcoin/0/): 0x00005555556a3f33 bench_bitcoin`... basic_string<char>::length(this=<unavailable>) const at basic_string.h:1079:16
Fixes
-
Automatic wallet teardown Wrap the benchmark in a
MakeWalletLoader
(owning aWalletContext
), so that both migrated wallets are unloaded when the loader goes out of scope, eliminating any lingering open files. -
Re-enable benchmarks in CI Drop the temporary filter in GitHub Actions. The
-sanity-check
run already executes each benchmark once, soWalletMigration
now runs automatically without hangs or crashes. -
Single iteration Configure the microbenchmark with
.epochs(1).epochIterations(1)
, ensuring the migration code runs exactly once and avoiding use-after-move.
No measurable change in benchmark performance.