- Sort imports and fix pyproject ruff config (per-file ignores for intentional Qt/core exceptions) - Mark Heirs.validate_* helpers as @staticmethod - Clean up dead code, rename shadowing vars, use raise ... from - Add AGENTS.md with env/lint/test/release guidance
122 lines
5.2 KiB
Python
122 lines
5.2 KiB
Python
"""
|
|
Focused regression/diagnostic test for the "anticipate near expiry" edge case.
|
|
|
|
Scenario reported by the plugin owner
|
|
-------------------------------------
|
|
When the user adds funds to the wallet, the plugin must rebuild the inheritance
|
|
transaction. To make the new transaction supersede the old one, the plugin
|
|
anticipates the locktime (moves it EARLIER) by a fixed amount (1 day), so the
|
|
new tx confirms before the old one and the old one gets invalidated.
|
|
|
|
That logic is fine when the will is far from its locktime. But if the will is
|
|
about to expire (e.g. locktime is only a few HOURS in the future), anticipating
|
|
by a full day pushes the new locktime INTO THE PAST.
|
|
|
|
This test verifies, against the real code, what value the anticipation logic
|
|
produces in that situation, and whether any guard prevents a past locktime.
|
|
|
|
It is a *diagnostic* test: it documents the current behaviour so we can decide
|
|
whether a fix is needed. Run:
|
|
|
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
|
python3 -m pytest tests/test_anticipate_past_locktime.py -q
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|
|
|
from bal.core.util import LOCKTIME_THRESHOLD, Util
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Pure-function level: Util.anticipate_locktime has no "now" lower bound.
|
|
# ---------------------------------------------------------------------------
|
|
def test_anticipate_locktime_can_fall_into_the_past():
|
|
"""A timestamp locktime only 10 hours away, anticipated by 1 day, lands
|
|
~14 hours in the PAST. The only clamp in anticipate_locktime is `out < 1`,
|
|
so a past-but-positive timestamp is returned unchanged."""
|
|
now = int(time.time())
|
|
locktime_in_10h = now + 10 * 3600 # expires in 10 hours
|
|
assert locktime_in_10h > LOCKTIME_THRESHOLD # it is a timestamp locktime
|
|
|
|
anticipated = int(Util.anticipate_locktime(locktime_in_10h, days=1))
|
|
|
|
# The anticipated locktime is earlier than the original (as intended)...
|
|
assert anticipated < locktime_in_10h
|
|
# ...but it is now in the PAST relative to "now":
|
|
assert anticipated < now, (
|
|
f"anticipated={anticipated} is not in the past relative to now={now}; "
|
|
"the edge case may have been fixed"
|
|
)
|
|
# And crucially it is NOT clamped to anything sensible like `now`; it is
|
|
# exactly original - 86400.
|
|
assert anticipated == locktime_in_10h - 86400
|
|
|
|
|
|
def test_anticipate_locktime_far_from_expiry_stays_in_future():
|
|
"""Control case: when the will is far from expiry (e.g. 30 days away),
|
|
anticipating by 1 day keeps the locktime safely in the future."""
|
|
now = int(time.time())
|
|
locktime_in_30d = now + 30 * 86400
|
|
anticipated = int(Util.anticipate_locktime(locktime_in_30d, days=1))
|
|
assert anticipated < locktime_in_30d
|
|
assert anticipated > now # still in the future -> safe
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. chk_locktime confirms the produced value is considered "expired".
|
|
# ---------------------------------------------------------------------------
|
|
def test_past_anticipated_locktime_is_seen_as_expired():
|
|
"""The anticipated past locktime, when fed to chk_locktime against the
|
|
current time, is reported as NOT in the future (i.e. already expired)."""
|
|
now = int(time.time())
|
|
locktime_in_10h = now + 10 * 3600
|
|
anticipated = int(Util.anticipate_locktime(locktime_in_10h, days=1))
|
|
|
|
# chk_locktime signature is now (timestamp_to_check, locktime) (A1):
|
|
# it returns True only if the locktime is still in the future.
|
|
in_future = Util.chk_locktime(now, anticipated)
|
|
assert in_future is False, (
|
|
"the anticipated locktime is unexpectedly still in the future"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Boundary scan: at what original distance does anticipation start to
|
|
# produce a past locktime? Documents the 24h threshold explicitly.
|
|
# ---------------------------------------------------------------------------
|
|
def test_boundary_is_exactly_24h():
|
|
now = int(time.time())
|
|
# Just under 24h away -> anticipated into the past.
|
|
just_under = now + 24 * 3600 - 60
|
|
a1 = int(Util.anticipate_locktime(just_under, days=1))
|
|
assert a1 < now
|
|
|
|
# Just over 24h away -> anticipated value stays (barely) in the future.
|
|
just_over = now + 24 * 3600 + 60
|
|
a2 = int(Util.anticipate_locktime(just_over, days=1))
|
|
assert a2 > now
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_anticipate_locktime_can_fall_into_the_past()
|
|
test_anticipate_locktime_far_from_expiry_stays_in_future()
|
|
test_past_anticipated_locktime_is_seen_as_expired()
|
|
test_boundary_is_exactly_24h()
|
|
|
|
# Human-readable demonstration
|
|
now = int(time.time())
|
|
for hours in (10, 23, 25, 48, 24 * 30):
|
|
lt = now + hours * 3600
|
|
a = int(Util.anticipate_locktime(lt, days=1))
|
|
delta_h = (a - now) / 3600.0
|
|
verdict = "PAST <-- problem" if a < now else "future (ok)"
|
|
print(
|
|
f"expires in {hours:>4}h -> anticipated locktime is "
|
|
f"{delta_h:+.1f}h from now [{verdict}]"
|
|
)
|
|
print("[OK] all anticipate-past-locktime diagnostics passed")
|