walletrpc: recover XCreateAccount funds on both address branches - #11096
walletrpc: recover XCreateAccount funds on both address branches#11096wieghx wants to merge 5 commits into
Conversation
XCreateAccount recovery previously told operators to replay a single address count with NextAddr. NextAddr defaults to the external branch, so that leaves internal/change scripts unknown after restore. Record and replay external_key_count and internal_key_count separately. Signed-off-by: wieghx <gudemail1991@gmail.com>
Recreate a non-trivial account index, leave value on an internal change output via FundPsbt, then restore from seed, replay both NextAddr branches, rescan, and spend. Signed-off-by: wieghx <gudemail1991@gmail.com>
🟡 PR Severity: MEDIUM
🟡 Medium (2 files)
🟢 Low (7 files)
AnalysisThe functional change is confined to a proto field addition for To override, add a |
Lrifton92
left a comment
There was a problem hiding this comment.
Nice change — the two-counter distinction is a real trap and the proto text now explains it clearly. I read
the branch and have one concrete inconsistency plus one question about the test.
The CLI note points at counters that are zero at that moment
The proto doc is explicit that the counters are useless at creation time:
Recording only the derivation path when the account is created is not enough, because both counters start
at zero and increase as the account is used.
But the CLI note printed by createAccount says the opposite in the same PR:
Record its derivation path and both external_key_count and internal_key_count (above) with your seed.
At that point printRespJSON(resp) has just printed an account whose external_key_count and
internal_key_count are both 0, and "(above)" refers to exactly that output. An operator who follows the
instruction literally records two zeros and is no better off than before this PR — which is the failure mode
the change is trying to prevent.
The same applies to the command's Description, which says to record "both external_key_count and
internal_key_count ... printed below".
I think the note wants to say something closer to: record the derivation path now, and read both counters
from ListAccounts before you need to restore, since they only become meaningful once the account has
issued addresses.
The test proves the remedy, never the failure
testXCreateAccountBranchRecovery establishes that replaying both branches recovers both. It never
establishes that replaying only the external branch does not — which is the claim the documentation now
rests on:
Replaying a single aggregate address count, or calling NextAddr without change=true, leaves internal/change
scripts unknown to the restored wallet even after a transaction rescan.
As written, the test would still pass if the internal-branch funds were recovered by some other mechanism and
the change=true replay were redundant. A short negative assertion — restore, replay only change=false,
rescan, and assert the internal output is absent — would make the documented claim load-bearing rather than
asserted. That is the part a future reader will want evidence for.
Question
RestoreNodeWithSeed(..., 0, nil) pins the recovery window to 0. Is that load-bearing for this scenario, or
incidental? My understanding from the existing doc is that the window would not help here anyway, since the
recovery scan only rederives the default account and the custom account does not exist until it is
re-created — but a one-line comment saying why 0 is the right value would save the next reader the
detour I just took.
The create-account CLI printed "record both counters (above)" right after XCreateAccount, when both counters are still zero. An operator following that note would store two zeros and recover nothing on the internal branch. Point the Description and the post-create note at ListAccounts for the counters, and keep the derivation path as the thing to record at creation time. Signed-off-by: wieghx <gudemail1991@gmail.com>
The branch-recovery itest only showed that replaying both NextAddr branches recovers both outputs. That would still pass if the internal change were found some other way. Restore, replay only change=false, rescan, and assert the internal output is absent; then replay change=true and rescan again. Also comment why RestoreNodeWithSeed uses RecoveryWindow 0: a non-zero window only rederives the default account. Signed-off-by: wieghx <gudemail1991@gmail.com>
|
Addressed the three items: CLI note pointing at zeros. The create-time response has Test only proved the remedy.
|
Lrifton92
left a comment
There was a problem hiding this comment.
Re-read at 4536b8b0. The CLI note and the RecoveryWindow comment both land — the note now records the path at create time and sends the operator to ListAccounts for the counters, which is the sequence that actually works.
The negative assertion is better than what I asked for: pinning the external-only balance to destAmt instead of "less than wantBal" makes the check fee-independent, so it will not drift when the FundPsbt fee changes. Hoisting destAmt into the const block rather than re-spelling fundAmt / 2 is the right call too.
One gap left, and it is in the new assertion rather than the old one.
The absence check is not synchronised with the rescan
AssertWalletAccountBalance is a wait.NoError poll (lntest/harness_assertion.go:2647) — it returns at the first tick where the confirmed balance equals the expected value. Nothing between it and
require.False(ht, sawInt, "internal change should still be unknown")waits for the rescan triggered by --reset-wallet-transactions to finish. So the snapshot establishes that the change is not visible yet, not that it cannot be found.
The exact match does most of the work: if a regression ever made the internal branch derivable during recovery, the settled balance would be wantBal, not destAmt, and the poll would time out instead of passing. But there is still a window — external credited, internal not yet — in which the balance is exactly destAmt and sawInt is legitimately false, and both checks pass on a rescan that is only half-done. That is the exact regression this test exists to catch, so it seems worth closing.
ht.WaitForBlockchainSync(restored) between the restart and the two assertions should be enough, assuming SyncedToChain only flips once the reset rescan has caught up. I am reasonably confident that holds but have not run it, so worth confirming rather than taking my word for it.
Nothing else from my side — the rest reads correct.
RestoreNodeWithSeed leaves SkipUnlock set, so RestartNode does not wait for SyncedToChain. After --reset-wallet-transactions that flag stays false until the wallet rescan reaches tip. Wait before the destAmt / sawInt checks so they observe a finished rescan rather than a dest-only intermediate. Signed-off-by: wieghx <gudemail1991@gmail.com>
|
The absence check was racing a still-running rescan, and it is a real gap — just not for the reason of a missing
Added (Dest and change live in the same mined tx, so a mid-rescan destAmt without change would still require the spend block to have been processed with only the external scripts in the address manager. Waiting for tip still closes the window the snapshot was taking.) |
Change Description
The recovery procedure added with
XCreateAccounttreated "how manyaddresses were issued" as a single count and told operators to replay
NextAddr. That RPC defaults to the external branch, so changeoutputs created by
FundPsbtstay unknown after a seed restore andrescan.
This updates the proto, CLI help, and post-create warning so operators
record
external_key_countandinternal_key_countseparately, replayNextAddron each branch, and only then rescan.Related to #11087.
Steps to Test
The new itest:
FundPsbt, leaving value on an internal change address.the original order.
NextAddrbranches, then rescans with--reset-wallet-transactions.Pull Request Checklist
Testing
Code Style and Documentation
[skip ci]in the commit message for small changes.AI assistance: I used an AI coding assistant while writing the docs and
itest. I reviewed the
NextAddr/FundPsbtpath and compiled./itestand the CLI package locally. I could not run the integrationtest here (no chain backend).
Signed-off-by: wieghx gudemail1991@gmail.com