fix(will): prevent double invalidation when postponing a signed will

When postponing the delivery time of an already signed/sent will, the user
was asked to sign the on-chain invalidation transaction twice before the new
(postponed) will could be built.

Root cause: after the invalidation tx was broadcast, on_success_invalidate
restarted task_phase1 to rebuild the will, but the old will items were still
marked COMPLETE/PUSHED with their original tx.locktime (the on-chain
invalidation did not update the in-memory status). The postpone check therefore
fired WillPostponedException a second time, requesting another invalidation.

Fix:
- Add Will.mark_invalidated_by_tx(will, tx): marks INVALIDATED every valid will
  item that spends a prevout consumed by the just-broadcast invalidation tx.
  Setting INVALIDATED clears the VALID flag, removing those items from
  only_valid_list so the postpone/expire check no longer fires.
- Call it from loop_broadcast_invalidating after a successful broadcast (txid
  obtained) and persist via save_willitems. On the phase-1 restart the old will
  is no longer VALID, so the will is rebuilt directly: a single invalidation
  signature followed by the new will.

Tests: add test_will_mark_invalidated_by_tx and
test_will_mark_invalidated_by_tx_no_match plus the WillPostponedException
hierarchy assertion. 184 tests pass; smoke and external-zip OK; ruff clean.

Bump version to 0.3.1.
This commit is contained in:
GenSpark AI Developer
2026-06-15 23:05:59 +00:00
committed by steal
parent 081e46515f
commit e477c5aa5b
8 changed files with 160 additions and 4 deletions

View File

@@ -708,6 +708,19 @@ class BalBuildWillDialog(BalDialog):
self.msg_set_invalidating(self.msg_ok())
if not txid:
_logger.debug(f"should not be none txid: {txid}")
else:
# The invalidation tx is now broadcast, so the old signed/sent
# will transactions spending those same UTXOs can no longer be
# mined. Mark them INVALIDATED (which clears their VALID flag)
# so the postpone/expire check does NOT fire a second
# invalidation when phase 1 is restarted to rebuild the new
# (postponed) will.
invalidated = Will.mark_invalidated_by_tx(
self.bal_window.willitems, tx
)
if invalidated:
_logger.debug(f"invalidated will items: {invalidated}")
self.bal_window.save_willitems()
except TxBroadcastError as e:
_logger.error(f"fail to broadcast transaction:{e}")