core: anchor relative locktime/threshold recipes to the built tx locktime so an unchanged will no longer reads as expired/postponed; stop the daily invalidate prompt (karen7)
This commit is contained in:
@@ -29,7 +29,10 @@ class CheckAliveError(Exception):
|
||||
|
||||
|
||||
def resolve_date_to_check(
|
||||
is_basic_mode: bool, will_settings: Any, now: float | None = None
|
||||
is_basic_mode: bool,
|
||||
will_settings: Any,
|
||||
now: float | None = None,
|
||||
built_locktime: float | int | None = None,
|
||||
) -> float:
|
||||
"""Return the reference timestamp for every will-validity check.
|
||||
|
||||
@@ -41,19 +44,56 @@ def resolve_date_to_check(
|
||||
govern those checks. ``date_to_check`` is set to *now*: every check is
|
||||
evaluated against the current moment (the Check Alive effectively does not
|
||||
exist) while the delivery locktime is still fully enforced.
|
||||
* ADVANCED mode: the user-controlled stored threshold is used as-is.
|
||||
* ADVANCED mode: the user-controlled stored threshold is used as-is. An
|
||||
ABSOLUTE threshold is returned unchanged; a RELATIVE one (``"30d"``/``"1y"``)
|
||||
means "N days BEFORE the delivery date" and is resolved against the stored
|
||||
locktime (matching the date the settings widget displays), so it stays in
|
||||
lockstep with the built transactions instead of drifting with the clock.
|
||||
|
||||
A RELATIVE stored locktime is resolved against the frozen delivery date of
|
||||
the built will (``built_locktime``, the locktime inside the signed tx) when
|
||||
one exists: the will's real delivery date is authoritative, and resolving
|
||||
the relative locktime from *now* would drift ``date_to_check`` past the
|
||||
frozen tx locktime so an unchanged will wrongly reads as expired (asking to
|
||||
invalidate) every day. Without a built will the legacy forward-from-now
|
||||
resolution is kept.
|
||||
|
||||
Args:
|
||||
is_basic_mode: ``True`` for the SIMPLE / BASIC user type.
|
||||
will_settings: the per-wallet settings dict (``"threshold"`` key).
|
||||
will_settings: the per-wallet settings dict (``"threshold"`` and
|
||||
``"locktime"`` keys).
|
||||
now: overridable clock for tests; defaults to ``datetime.now()``.
|
||||
built_locktime: the absolute locktime frozen inside the built will's
|
||||
transactions (``None`` when there is no built will yet).
|
||||
|
||||
Returns:
|
||||
The reference timestamp (float, UNIX seconds).
|
||||
"""
|
||||
if is_basic_mode:
|
||||
return (now if now is not None else datetime.now().timestamp())
|
||||
return BalTimestamp(will_settings["threshold"]).to_timestamp()
|
||||
|
||||
threshold = BalTimestamp(will_settings["threshold"])
|
||||
# A RELATIVE threshold ("30d"/"1y") means "N days BEFORE the delivery":
|
||||
# the settings widget resolves it as real_threshold = locktime - N days.
|
||||
# Resolving it FORWARD from now (BalTimestamp.to_timestamp) turns
|
||||
# date_to_check into a moving target that disagrees with the fixed
|
||||
# locktime of the built transactions (and with the date shown in the UI),
|
||||
# which can wrongly mark the will as expired/postponed. Resolve it
|
||||
# against the delivery date instead.
|
||||
if threshold.unit is not None:
|
||||
locktime_raw = will_settings.get("locktime")
|
||||
if locktime_raw is None:
|
||||
# No delivery reference to anchor to: fall back to the legacy
|
||||
# forward-from-now resolution.
|
||||
return threshold.to_timestamp()
|
||||
locktime_dt = BalTimestamp(locktime_raw).to_date(now)
|
||||
# A RELATIVE stored locktime ("2y") is itself a moving target; when a
|
||||
# will has already been built, its frozen delivery date (the tx
|
||||
# locktime) is the authoritative anchor (see docstring).
|
||||
if BalTimestamp(locktime_raw).unit is not None and built_locktime:
|
||||
locktime_dt = BalTimestamp(int(built_locktime)).to_date(now)
|
||||
return threshold.to_date(locktime_dt, reverse=True).timestamp()
|
||||
return threshold.to_timestamp()
|
||||
|
||||
|
||||
def check_alive_expired(
|
||||
|
||||
@@ -131,6 +131,72 @@ class Util:
|
||||
+ days * 60 * 60 * 24
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _relative_days(value):
|
||||
"""Duration in days of a relative ``"Nd"``/``"Ny"`` recipe.
|
||||
|
||||
Returns ``None`` when the value is not a relative recipe (an absolute
|
||||
timestamp, a plain number, or garbage).
|
||||
"""
|
||||
s = str(value)
|
||||
if s and s[-1] in "yYdD":
|
||||
try:
|
||||
n = int(s[:-1])
|
||||
except ValueError:
|
||||
return None
|
||||
return n * 365 if s[-1] in "yY" else n
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def resolve_locktime_against_tx(current, built, tx_locktime):
|
||||
"""Resolve a locktime recipe against the moment the signed tx was built.
|
||||
|
||||
A RELATIVE recipe stored in the wallet (``"1y"``/``"30d"``) is a moving
|
||||
target: parsing it against *now* on every check drifts it one day per
|
||||
day away from the fixed locktime frozen inside the signed Bitcoin
|
||||
transaction, so an UNCHANGED will is mistaken for a POSTPONE and the
|
||||
plugin asks to invalidate it every day (reported bug). This resolves
|
||||
the current recipe against the build moment instead, recovered from the
|
||||
signed transaction's locktime and the recipe that was actually frozen
|
||||
at build time (``built``, the value stored in the will item):
|
||||
|
||||
build_moment = tx_locktime - duration(built)
|
||||
expected = build_moment + duration(current)
|
||||
|
||||
An unchanged recipe therefore resolves to exactly ``tx_locktime``
|
||||
(coherent), a lengthened one resolves later (postpone) and a shortened
|
||||
one earlier (anticipate).
|
||||
|
||||
Args:
|
||||
current: the current locktime recipe (relative or absolute).
|
||||
built: the recipe frozen at build time (stored in the will item).
|
||||
tx_locktime: the absolute locktime frozen inside the signed tx.
|
||||
|
||||
Returns:
|
||||
int: the resolved absolute locktime (UNIX timestamp).
|
||||
"""
|
||||
current_days = Util._relative_days(current)
|
||||
built_days = Util._relative_days(built)
|
||||
if current_days is None:
|
||||
# Absolute current date: compare directly against the frozen tx.
|
||||
try:
|
||||
return int(current)
|
||||
except Exception:
|
||||
return Util.parse_locktime_string(current)
|
||||
if built_days is None or not tx_locktime:
|
||||
# The stored recipe was absolute (a fixed date) or the tx has no
|
||||
# usable locktime: there is no relative anchor to recover the build
|
||||
# moment, so fall back to the legacy forward-from-now resolution.
|
||||
return Util.parse_locktime_string(current)
|
||||
try:
|
||||
base = datetime.fromtimestamp(int(tx_locktime)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
build_moment = base - timedelta(days=built_days)
|
||||
return int((build_moment + timedelta(days=current_days)).timestamp())
|
||||
except Exception:
|
||||
return Util.parse_locktime_string(current)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Amount helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -1144,8 +1144,6 @@ class Will:
|
||||
if heir := heirs.get(wheir, None):
|
||||
|
||||
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].
|
||||
@@ -1156,6 +1154,16 @@ class Will:
|
||||
# undetected. w.tx.locktime is immutable once signed
|
||||
# and is exactly what the will-executors hold.
|
||||
tx_locktime = int(w.tx.locktime)
|
||||
# The requested (possibly new) locktime for this heir.
|
||||
# A RELATIVE recipe ("1y"/"30d") is resolved against
|
||||
# the moment the signed tx was built, NOT against now:
|
||||
# re-parsing it from "now" drifts it one day per day
|
||||
# away from the frozen tx locktime, so an UNCHANGED
|
||||
# will would be read as a POSTPONE and the plugin
|
||||
# would ask to invalidate it every day.
|
||||
new_locktime = Util.resolve_locktime_against_tx(
|
||||
heir[2], their[2], tx_locktime
|
||||
)
|
||||
if new_locktime == tx_locktime:
|
||||
# Unchanged: this heir is still coherent.
|
||||
count = heirs_found.get(wheir, 0)
|
||||
|
||||
Reference in New Issue
Block a user