forked from bitcoinafterlife/bal-electrum-plugin
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:
committed by
steal
parent
081e46515f
commit
e477c5aa5b
@@ -228,6 +228,79 @@ def test_will_check_tx_height():
|
||||
# Exception classes
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_will_mark_invalidated_by_tx():
|
||||
"""A valid will spending the same prevout as the invalidation tx must be
|
||||
marked INVALIDATED (and therefore lose its VALID flag). This is what
|
||||
prevents the postpone/expire check from firing a *second* invalidation
|
||||
when phase 1 is restarted after a successful on-chain invalidation."""
|
||||
class FakePrevout:
|
||||
def __init__(self, s):
|
||||
self._s = s
|
||||
def to_str(self):
|
||||
return self._s
|
||||
|
||||
class FakeInput:
|
||||
def __init__(self, s):
|
||||
self.prevout = FakePrevout(s)
|
||||
|
||||
class FakeTx:
|
||||
def __init__(self, prevouts):
|
||||
self._inputs = [FakeInput(p) for p in prevouts]
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
# The real test will item spends this prevout (from _VALID_TX_HEX).
|
||||
spent = "3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0"
|
||||
|
||||
# Will item that spends the same UTXO -> must be invalidated.
|
||||
item_match = _make_willitem_blank()
|
||||
item_match.set_status("COMPLETE", True)
|
||||
# Will item that spends an unrelated UTXO -> must stay VALID.
|
||||
item_other = _make_willitem_blank()
|
||||
item_other.tx = FakeTx(["deadbeef:1"])
|
||||
item_other.children = {}
|
||||
|
||||
will = {"match": item_match, "other": item_other}
|
||||
inval_tx = FakeTx([spent])
|
||||
|
||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
||||
|
||||
assert "match" in invalidated
|
||||
assert "other" not in invalidated
|
||||
assert will["match"].get_status("INVALIDATED") is True
|
||||
assert will["match"].get_status("VALID") is False
|
||||
assert will["other"].get_status("VALID") is True
|
||||
|
||||
|
||||
def test_will_mark_invalidated_by_tx_no_match():
|
||||
"""If no valid will spends any of the invalidation tx's prevouts, nothing
|
||||
is marked."""
|
||||
class FakePrevout:
|
||||
def __init__(self, s):
|
||||
self._s = s
|
||||
def to_str(self):
|
||||
return self._s
|
||||
|
||||
class FakeInput:
|
||||
def __init__(self, s):
|
||||
self.prevout = FakePrevout(s)
|
||||
|
||||
class FakeTx:
|
||||
def __init__(self, prevouts):
|
||||
self._inputs = [FakeInput(p) for p in prevouts]
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
item = _make_willitem_blank()
|
||||
will = {"a": item}
|
||||
inval_tx = FakeTx(["unrelated:9"])
|
||||
|
||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
||||
|
||||
assert invalidated == []
|
||||
assert will["a"].get_status("VALID") is True
|
||||
|
||||
|
||||
def test_exceptions():
|
||||
from bal.core.will import (
|
||||
WillException, WillExpiredException, NotCompleteWillException,
|
||||
@@ -235,6 +308,7 @@ def test_exceptions():
|
||||
WillexecutorChangeException, NoWillExecutorNotPresent,
|
||||
WillExecutorNotPresent, NoHeirsException,
|
||||
AmountException, PercAmountException, FixedAmountException,
|
||||
WillPostponedException,
|
||||
)
|
||||
|
||||
assert issubclass(WillExpiredException, WillException)
|
||||
@@ -248,6 +322,9 @@ def test_exceptions():
|
||||
assert issubclass(NoHeirsException, WillException)
|
||||
assert issubclass(PercAmountException, AmountException)
|
||||
assert issubclass(FixedAmountException, AmountException)
|
||||
# WillPostponedException is a NotCompleteWillException but MUST be caught
|
||||
# before it in task_phase1, so it triggers an on-chain invalidation.
|
||||
assert issubclass(WillPostponedException, NotCompleteWillException)
|
||||
|
||||
# WillException default message
|
||||
exc = WillException()
|
||||
|
||||
Reference in New Issue
Block a user