feat(will): invalidate signed will on postpone + add Server status column

Postpone safety (Strategy B):
- A signed/sent will carries an immutable locktime; postponing the delivery
  time previously did nothing, so a will-executor could still broadcast the
  old (earlier-locktime) transaction and execute the inheritance too early.
- core/will.py: add WillPostponedException and detect postpone by comparing
  the requested locktime against w.tx.locktime (the locktime frozen in the
  signed transaction) instead of the in-memory heir entry, which is updated
  together with the new value and would always compare equal.
- gui/qt/dialogs.py (BalBuildWillDialog.task_phase1, the real path used by
  Tools -> Prepare): handle WillPostponedException before NotCompleteWill;
  return (None, tx) to trigger sign + broadcast of the invalidation, then the
  user presses Prepare again to rebuild/re-sign/re-send (two explicit steps).
- gui/qt/window.py: mirror the branch in build_inheritance_transaction with an
  explanatory message; wording aligned to the 'Prepare' button.
- gui/qt/common.py: export WillPostponedException.
- A postpone on a will that was never signed/sent just rebuilds (no on-chain
  fee).

Server status column:
- gui/qt/lists.py: add a dedicated 'Server' column to PreviewList with an
  always-readable label and a tooltip (will-executor URL + state).
- gui/qt/theme.py: add server_status_text() and server_status_tooltip(),
  reusing the existing status flags.
- gui/qt/common.py: export the new theme helpers.

Docs: update README.md, bal/README.md and CHANGELOG_REFACTOR.md.

Tests: 182 passed; smoke + external-zip OK; ruff has no new real findings.
This commit is contained in:
GenSpark AI Developer
2026-06-15 21:52:22 +00:00
parent 714b17eacd
commit a394cde0b5
9 changed files with 263 additions and 10 deletions

View File

@@ -671,14 +671,41 @@ class Will:
their = will[wid].heirs[wheir]
if heir := heirs.get(wheir, None):
if (
heir[0] == their[0]
and heir[1] == their[1]
and Util.parse_locktime_string(heir[2])
>= Util.parse_locktime_string(their[2])
):
count = heirs_found.get(wheir, 0)
heirs_found[wheir] = count + 1
if heir[0] == their[0] and heir[1] == their[1]:
# The requested (possibly new) locktime for this heir.
new_locktime = Util.parse_locktime_string(heir[2])
# IMPORTANT: compare against the locktime that is
# actually frozen inside the already-signed Bitcoin
# transaction (w.tx.locktime), NOT against their[2].
# their[2] is the heir entry stored in the will item,
# which is updated in memory together with the new
# heirs dict when the user postpones, so it would
# always equal new_locktime and the postpone would go
# undetected. w.tx.locktime is immutable once signed
# and is exactly what the will-executors hold.
tx_locktime = int(w.tx.locktime)
if new_locktime == tx_locktime:
# Unchanged: this heir is still coherent.
count = heirs_found.get(wheir, 0)
heirs_found[wheir] = count + 1
elif new_locktime > tx_locktime and (
w.get_status("COMPLETE") or w.get_status("PUSHED")
):
# POSTPONE of an already signed/sent will: the
# old pre-signed tx must be invalidated on-chain
# first, otherwise a will-executor could
# broadcast the earlier-locktime tx and execute
# the inheritance too early.
raise WillPostponedException(
f"{wheir}: locktime postponed "
f"{tx_locktime}->{new_locktime} "
f"on a signed/sent will"
)
# new_locktime < tx_locktime (anticipate) is left to
# check_will_expired -> WillExpiredException.
# new_locktime > tx_locktime on a will that was never
# signed/sent falls through here -> a plain rebuild via
# HeirNotFoundException (no on-chain fee needed).
else:
_logger.debug(
f"heir not present transaction is not valid:{wheir} {wid}, {w}"
@@ -912,6 +939,21 @@ class HeirNotFoundException(NotCompleteWillException):
pass
class WillPostponedException(NotCompleteWillException):
"""An already signed/sent will is being postponed.
When a will that has already been signed (``COMPLETE``) and/or pushed to
will-executors (``PUSHED``) gets its locktime moved to a LATER date, the
previously committed coins must be invalidated on-chain BEFORE rebuilding
the new inheritance. Otherwise a will-executor could broadcast the old
(earlier-locktime) transaction and execute the inheritance too early to
collect the fees. Invalidating spends the same UTXOs now, permanently
voiding the old pre-signed transaction.
"""
pass
class WillexecutorChangeException(NotCompleteWillException):
pass