wallet: the file-name picker must consult the disk, not only the catalogue #151
Loading…
Reference in a new issue
No description provided.
Delete branch "onionwire/t_81deabb5-wallet-the-file-name-picker-must-consult"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The picker now looks at the disk, not only at the catalogue.
The bug
create_wallet onionwire-1 -> (code -21, "Cannot create wallet. Already exists.")next_wallet_filenamereturned<default>-<n>for the firstnthe SQLCipher catalogue did not hold and never looked at the wallet directory. Two ways the disk gets ahead of the database:Store::delete_walletdrops the catalogue row and keeps the file (its own doc comment says so).record_walletfailed leaves a file with no row.So the picker proposed a name whose
<name>and<name>.keyswere already on disk, the daemon refused with-21, and because the catalogue never changed, the next attempt proposed the same name again. A dead end with no UI escape.The change
wallet_child::wallet_name_taken(wallet_dir, name)(src/wallet_child.rs): lists the wallet dir and answers whether<name>or its.keyssibling is already there. Reads only - it deletes nothing.Node::next_wallet_filenamenow requires the candidate to be free in both places: the catalogue and the disk.Node::wallet_dir()is now the single source of$ONIONWIRE_HOME/wallet; the child spawn uses it too, so the picker and the spawned child can never disagree about the directory.Fail closed:
read_dirfailing (missing dir, or a path that is a file) is an error out ofwallet_name_taken, propagated bynext_wallet_filenameaswallet dir <path>: <os error>- never "every name is free".Second commit: the test harness race that made the first push red
The first push of this branch (
59175f6) failedci / teston two pre-existing tests from the parent PR, both green when run alone:wallet_child::tests::loopback_and_clearnet_remotes_carry_no_proxy_and_spawn_no_socks_child: the printed argv ended with a bare--daemon-address.node::tests::a_restart_opens_the_stored_wallet_file:wallet RPC never answered (Connection refused).8d80a4ffixes the first one, and it is now green in CI (run 905: that test passes). Cause: the stand-in scripts (fake_bin,fake_child_bin) truncated their argv log and appended one line per argument, while the tests poll that log to learn the port. A reader can catch a prefix. The argv now goes into a temp file and ismvd into place, so a reader sees either no log or the complete one. Both hunks are inside#[cfg(test)] mod tests; no production code changed. Measured, not assumed (argv-race-probe.sh, 300 tight-loop reads of each variant): the append version was caught mid-write 1/300 times, themvversion 0/300.Still red, and not this card's change:
a_restart_opens_the_stored_wallet_filealso failed in run 905 (its second CI run), on the ARM runner only. It passes locally every time - 3/3 isolated, and in a fullcargo testrun - which is the load-sensitivity signature this card's own review flagged rather than a logic failure in the picker. It is the parent PR's test (added by #149, green in #149's own CI) and its own harness race, notnext_wallet_filename: this diff adds no dial path and touches no code it exercises beyond a privatewallet_dir()helper that returns exactly the samehome.join("wallet")the spawn used before. The honest state of this branch is: the picker change and the wallet_child harness fix are verified; that one parent test needs its own pass on the runner.Out of scope, deliberately not done
No file is deleted. Orphaned wallet files are not cleaned up: a file the catalogue does not hold may be the only copy of a funded wallet, so the only job here is to avoid colliding with it. The single
fs::remove_dir_allin the diff is in a test body, removing the tempdir's own wallet dir to exercise the fail-closed path.Tests
src/node.rs(#[cfg(test)] mod tests, becausetests/*.rscannot build aNode):wallet_files_on_disk_are_skipped_when_picking_a_name- a strayonionwire-1.keysand a strayonionwire-2, no catalogue rows; the create proposesonionwire-3. This is the reported bug.a_catalogue_name_is_still_skipped_by_the_picker- the catalogue check is an addition, not a replacement.an_unrelated_file_on_disk_does_not_cost_the_first_name- the check is about the candidate, not about the dir being busy.the_wallet_name_picker_fails_closed_when_the_dir_cannot_be_read- the wallet dir removed; the create refuses with awallet direrror and records nothing.tests/wallet.rs(the helper ispub, noNodeneeded):wallet_name_taken_sees_the_file_and_its_keys_sibling-<name>,<name>.keysare taken;onionwire(a prefix) andonionwire-20(a longer name) are not; the two stray files are still on disk afterwards.wallet_name_taken_fails_closed_when_the_dir_cannot_be_read- a missing dir and a path that is a file both error.Neighbouring tests use
tempfile::tempdir(), and so do these.Verification
cargo test: exit 0, 35 test binaries green (92 lib tests, 0 failed).cargo clippy ... -- -D warnings: exit 0, zero warnings.rustfmt --edition 2024 --checkhunk count is unchanged from the base for both files (node.rs 3, wallet_child.rs 2 - all inherited).Mutation check (the picker tests have teeth). Replacing the disk check with
let _ = &dir;fails bothwallet_files_on_disk_are_skipped_when_picking_a_name(assertion left == right failed: the stray .keys file and the stray wallet file are both taken) andthe_wallet_name_picker_fails_closed_when_the_dir_cannot_be_read. Restored, they pass.One state worth knowing
The app-owned child is the only thing that runs
--wallet-dir $ONIONWIRE_HOME/wallet, andWalletChild::newcreates that directory 0700. An install whose F7/env config points at an external wallet RPC and has never started the app-owned child has no$ONIONWIRE_HOME/walletdir, so a create there now refuses withwallet dir ...: No such file or directoryinstead of proposing a name the external daemon may already hold. That is the deliberate fail-closed reading of this card; the fix is to start the app-owned child once (which creates the dir), and the error names the directory it could not list.Stacking
Based on
onionwire/t_67879b98-wallet-actually-open-the-wallet-file-and(PR #149), per the card's stacking note:git rev-list --count origin/main..origin/onionwire/t_67879b98-wallet-actually-open-the-wallet-file-andis 1, so the parent is still open. This PR sharessrc/node.rsandsrc/wallet_child.rswith it and must be retargeted tomainonce #149 lands.The wallet-child tests learn a spawned stand-in's port by polling its argv log, and `fake_bin`/`fake_child_bin` truncated the log and then appended one line per argument -- so a poller could read a prefix. A truncated port is a *valid* u16 ("427" out of "42765"), so `wait_for_child_rpc_port` happily returned a port nothing was listening on and the restart's `open_wallet` died with `Connection refused` after its 10s budget; the same prefix read is what `loopback_and_clearnet_remotes_carry_no_proxy...` printed with its `--daemon-address` value missing. Both tests failed on the first CI run of this branch (ci / test) and are green in isolation, which is the signature of this race rather than of a code defect. The argv now lands in the log with a single `mv`, so a reader sees either no log or the complete one. Measured with a tight read loop (argv-race-probe.sh): the append version was caught mid-write 1/300 reads, the mv version 0/300.Head moved
8d80a4fd→6edcd65e("tests: take the restart's port from the child that dials it"), cherry-picked in from #152 so this PR'sci / testis not left red on a harness race that nothing in this PR touches.What was red:
node::tests::a_restart_opens_the_stored_wallet_file(run 587, both attempts) —wallet connect: Connection refused (os error 111). The restarted child's mock was bound on the port parsed out of the sharedargv.log, which the killed child's late write can land after the test clears it. The picker change is untouched by it: same sha59175f6was red in run 583 and green in run 584 with no code change, and all four new picker tests pass.6edcd65eis the tree that ranci / testgreen in run 589. #152 is closed as absorbed.