1
0

lnutil: make UpdateAddHtlc dataclass

it is straightforward to move UpdateAddHtlc away from attr
to a dataclass without requiring any db update.
This commit is contained in:
f321x
2025-09-11 15:18:13 +02:00
committed by SomberNight
parent 4c0155c072
commit e6ea6dbf0a
3 changed files with 102 additions and 77 deletions

View File

@@ -17,6 +17,7 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import dataclasses
import enum import enum
from collections import defaultdict from collections import defaultdict
from enum import IntEnum, Enum from enum import IntEnum, Enum
@@ -1202,12 +1203,10 @@ class Channel(AbstractChannel):
"""Adds a new LOCAL HTLC to the channel. """Adds a new LOCAL HTLC to the channel.
Action must be initiated by LOCAL. Action must be initiated by LOCAL.
""" """
if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc) assert isinstance(htlc, UpdateAddHtlc)
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat) self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat)
if htlc.htlc_id is None: if htlc.htlc_id is None:
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL)) htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
with self.db_lock: with self.db_lock:
self.hm.send_htlc(htlc) self.hm.send_htlc(htlc)
self.logger.info("add_htlc") self.logger.info("add_htlc")
@@ -1217,15 +1216,13 @@ class Channel(AbstractChannel):
"""Adds a new REMOTE HTLC to the channel. """Adds a new REMOTE HTLC to the channel.
Action must be initiated by REMOTE. Action must be initiated by REMOTE.
""" """
if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc) assert isinstance(htlc, UpdateAddHtlc)
try: try:
self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat) self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat)
except PaymentFailure as e: except PaymentFailure as e:
raise RemoteMisbehaving(e) from e raise RemoteMisbehaving(e) from e
if htlc.htlc_id is None: # used in unit tests if htlc.htlc_id is None: # used in unit tests
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE)) htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE))
with self.db_lock: with self.db_lock:
self.hm.recv_htlc(htlc) self.hm.recv_htlc(htlc)
if onion_packet: if onion_packet:

View File

@@ -12,9 +12,10 @@ from functools import lru_cache
import electrum_ecc as ecc import electrum_ecc as ecc
from electrum_ecc import CURVE_ORDER, ecdsa_sig64_from_der_sig from electrum_ecc import CURVE_ORDER, ecdsa_sig64_from_der_sig
from electrum_ecc.util import bip340_tagged_hash from electrum_ecc.util import bip340_tagged_hash
import dataclasses
import attr import attr
from .util import bfh, UserFacingException, list_enabled_bits from .util import bfh, UserFacingException, list_enabled_bits, is_hex_str
from .util import ShortID as ShortChannelID, format_short_id as format_short_channel_id from .util import ShortID as ShortChannelID, format_short_id as format_short_channel_id
from .crypto import sha256, pw_decode_with_version_and_mac from .crypto import sha256, pw_decode_with_version_and_mac
@@ -22,7 +23,8 @@ from .transaction import (
Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, OPPushDataPubkey Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, OPPushDataPubkey
) )
from . import bitcoin, crypto, transaction, descriptor, segwit_addr from . import bitcoin, crypto, transaction, descriptor, segwit_addr
from .bitcoin import redeem_script_to_address, address_to_script, construct_witness, construct_script from .bitcoin import redeem_script_to_address, address_to_script, construct_witness, \
construct_script, NLOCKTIME_BLOCKHEIGHT_MAX
from .i18n import _ from .i18n import _
from .bip32 import BIP32Node, BIP32_PRIME from .bip32 import BIP32Node, BIP32_PRIME
from .transaction import BCDataStream, OPPushDataGeneric from .transaction import BCDataStream, OPPushDataGeneric
@@ -1908,26 +1910,37 @@ NUM_MAX_HOPS_IN_PAYMENT_PATH = 20
NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH
@attr.s(frozen=True) @dataclasses.dataclass(frozen=True, kw_only=True)
class UpdateAddHtlc: class UpdateAddHtlc:
amount_msat = attr.ib(type=int, kw_only=True) amount_msat: int
payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes, repr=lambda val: val.hex()) payment_hash: bytes
cltv_abs = attr.ib(type=int, kw_only=True) cltv_abs: int
timestamp = attr.ib(type=int, kw_only=True) htlc_id: Optional[int] = dataclasses.field(default=None)
htlc_id = attr.ib(type=int, kw_only=True, default=None) timestamp: int = dataclasses.field(default_factory=lambda: int(time.time()))
@staticmethod @staticmethod
@stored_in('adds', tuple) @stored_in('adds', tuple)
def from_tuple(amount_msat, payment_hash, cltv_abs, htlc_id, timestamp) -> 'UpdateAddHtlc': def from_tuple(amount_msat, rhash, cltv_abs, htlc_id, timestamp) -> 'UpdateAddHtlc':
return UpdateAddHtlc( return UpdateAddHtlc(
amount_msat=amount_msat, amount_msat=amount_msat,
payment_hash=payment_hash, payment_hash=bytes.fromhex(rhash),
cltv_abs=cltv_abs, cltv_abs=cltv_abs,
htlc_id=htlc_id, htlc_id=htlc_id,
timestamp=timestamp) timestamp=timestamp)
def to_json(self): def to_json(self):
return self.amount_msat, self.payment_hash, self.cltv_abs, self.htlc_id, self.timestamp self._validate()
return dataclasses.astuple(self)
def _validate(self):
assert isinstance(self.amount_msat, int), self.amount_msat
assert isinstance(self.payment_hash, bytes) and len(self.payment_hash) == 32
assert isinstance(self.cltv_abs, int) and self.cltv_abs <= NLOCKTIME_BLOCKHEIGHT_MAX, self.cltv_abs
assert isinstance(self.htlc_id, int) or self.htlc_id is None, self.htlc_id
assert isinstance(self.timestamp, int), self.timestamp
def __post_init__(self):
self._validate()
class OnionFailureCodeMetaFlag(IntFlag): class OnionFailureCodeMetaFlag(IntFlag):

View File

@@ -27,6 +27,7 @@ import os
import binascii import binascii
from pprint import pformat from pprint import pformat
import logging import logging
import dataclasses
from electrum import bitcoin from electrum import bitcoin
from electrum import lnpeer from electrum import lnpeer
@@ -257,31 +258,34 @@ class TestChannel(ElectrumTestCase):
self.paymentPreimage = b"\x01" * 32 self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage) paymentHash = bitcoin.sha256(self.paymentPreimage)
self.htlc_dict = { self.htlc = UpdateAddHtlc(
'payment_hash': paymentHash, payment_hash=paymentHash,
'amount_msat': one_bitcoin_in_msat, amount_msat=one_bitcoin_in_msat,
'cltv_abs': 5, cltv_abs=5,
'timestamp': 0, timestamp=0,
} )
# First Alice adds the outgoing HTLC to her local channel's state # First Alice adds the outgoing HTLC to her local channel's state
# update log. Then Alice sends this wire message over to Bob who adds # update log. Then Alice sends this wire message over to Bob who adds
# this htlc to his remote state update log. # this htlc to his remote state update log.
self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict).htlc_id self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc).htlc_id
self.assertNotEqual(list(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1).values()), []) self.assertNotEqual(list(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1).values()), [])
before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE) before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE)
beforeLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL) beforeLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL)
self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc_dict).htlc_id self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc).htlc_id
self.htlc = self.bob_channel.hm.log[REMOTE]['adds'][0] self.htlc = self.bob_channel.hm.log[REMOTE]['adds'][0]
def test_concurrent_reversed_payment(self): def test_concurrent_reversed_payment(self):
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') self.htlc = dataclasses.replace(
self.htlc_dict['amount_msat'] += 1000 self.htlc,
self.bob_channel.add_htlc(self.htlc_dict) payment_hash=bitcoin.sha256(32 * b'\x02'),
self.alice_channel.receive_htlc(self.htlc_dict) amount_msat=self.htlc.amount_msat + 1000,
)
self.bob_channel.add_htlc(self.htlc)
self.alice_channel.receive_htlc(self.htlc)
self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(LOCAL)) self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(LOCAL))
self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL)) self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL))
@@ -561,9 +565,12 @@ class TestChannel(ElectrumTestCase):
tx6 = str(alice_channel.force_close_tx()) tx6 = str(alice_channel.force_close_tx())
self.assertNotEqual(tx5, tx6) self.assertNotEqual(tx5, tx6)
self.htlc_dict['amount_msat'] *= 5 self.htlc = dataclasses.replace(
bob_index = bob_channel.add_htlc(self.htlc_dict).htlc_id self.htlc,
alice_index = alice_channel.receive_htlc(self.htlc_dict).htlc_id amount_msat=self.htlc.amount_msat * 5,
)
bob_index = bob_channel.add_htlc(self.htlc).htlc_id
alice_index = alice_channel.receive_htlc(self.htlc).htlc_id
force_state_transition(bob_channel, alice_channel) force_state_transition(bob_channel, alice_channel)
@@ -662,18 +669,26 @@ class TestChannel(ElectrumTestCase):
self.alice_to_bob_fee_update(0) self.alice_to_bob_fee_update(0)
force_state_transition(self.alice_channel, self.bob_channel) force_state_transition(self.alice_channel, self.bob_channel)
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') self.htlc = dataclasses.replace(
self.alice_channel.add_htlc(self.htlc_dict) self.htlc,
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03') payment_hash=bitcoin.sha256(32 * b'\x02'),
self.alice_channel.add_htlc(self.htlc_dict) )
self.alice_channel.add_htlc(self.htlc)
self.htlc = dataclasses.replace(
self.htlc,
payment_hash=bitcoin.sha256(32 * b'\x03'),
)
self.alice_channel.add_htlc(self.htlc)
# now there are three htlcs (one was in setUp) # now there are three htlcs (one was in setUp)
# Alice now has an available balance of 2 BTC. We'll add a new HTLC of # Alice now has an available balance of 2 BTC. We'll add a new HTLC of
# value 2 BTC, which should make Alice's balance negative (since she # value 2 BTC, which should make Alice's balance negative (since she
# has to pay a commitment fee). # has to pay a commitment fee).
new = dict(self.htlc_dict) new = dataclasses.replace(
new['amount_msat'] *= 2.5 self.htlc,
new['payment_hash'] = bitcoin.sha256(32 * b'\x04') amount_msat=int(self.htlc.amount_msat * 2.5),
payment_hash=bitcoin.sha256(32 * b'\x04'),
)
with self.assertRaises(lnutil.PaymentFailure) as cm: with self.assertRaises(lnutil.PaymentFailure) as cm:
self.alice_channel.add_htlc(new) self.alice_channel.add_htlc(new)
self.assertIn('Not enough local balance', cm.exception.args[0]) self.assertIn('Not enough local balance', cm.exception.args[0])
@@ -822,14 +837,14 @@ class TestChanReserve(ElectrumTestCase):
# Bob: 5.0 # Bob: 5.0
paymentPreimage = b"\x01" * 32 paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage) paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = { htlc = UpdateAddHtlc(
'payment_hash': paymentHash, payment_hash=paymentHash,
'amount_msat': int(.5 * one_bitcoin_in_msat), amount_msat=int(.5 * one_bitcoin_in_msat),
'cltv_abs': 5, cltv_abs=5,
'timestamp': 0, timestamp=0,
} )
self.alice_channel.add_htlc(htlc_dict) self.alice_channel.add_htlc(htlc)
self.bob_channel.receive_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc)
# Force a state transition, making sure this HTLC is considered valid # Force a state transition, making sure this HTLC is considered valid
# even though the channel reserves are not met. # even though the channel reserves are not met.
force_state_transition(self.alice_channel, self.bob_channel) force_state_transition(self.alice_channel, self.bob_channel)
@@ -847,10 +862,10 @@ class TestChanReserve(ElectrumTestCase):
# Alice: 4.5 # Alice: 4.5
# Bob: 5.0 # Bob: 5.0
with self.assertRaises(lnutil.PaymentFailure): with self.assertRaises(lnutil.PaymentFailure):
htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') htlc = dataclasses.replace(htlc, payment_hash=bitcoin.sha256(32 * b'\x02'))
self.bob_channel.add_htlc(htlc_dict) self.bob_channel.add_htlc(htlc)
with self.assertRaises(lnutil.RemoteMisbehaving): with self.assertRaises(lnutil.RemoteMisbehaving):
self.alice_channel.receive_htlc(htlc_dict) self.alice_channel.receive_htlc(htlc)
def part2(self): def part2(self):
paymentPreimage = b"\x01" * 32 paymentPreimage = b"\x01" * 32
@@ -861,22 +876,22 @@ class TestChanReserve(ElectrumTestCase):
# Resulting balances: # Resulting balances:
# Alice: 1.5 # Alice: 1.5
# Bob: 9.5 # Bob: 9.5
htlc_dict = { htlc = UpdateAddHtlc(
'payment_hash': paymentHash, payment_hash=paymentHash,
'amount_msat': int(3.5 * one_bitcoin_in_msat), amount_msat=int(3.5 * one_bitcoin_in_msat),
'cltv_abs': 5, cltv_abs=5,
} )
self.alice_channel.add_htlc(htlc_dict) self.alice_channel.add_htlc(htlc)
self.bob_channel.receive_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc)
# Add a second HTLC of 1 BTC. This should fail because it will take # Add a second HTLC of 1 BTC. This should fail because it will take
# Alice's balance all the way down to her channel reserve, but since # Alice's balance all the way down to her channel reserve, but since
# she is the initiator the additional transaction fee makes her # she is the initiator the additional transaction fee makes her
# balance dip below. # balance dip below.
htlc_dict['amount_msat'] = one_bitcoin_in_msat htlc = dataclasses.replace(htlc, amount_msat=one_bitcoin_in_msat)
with self.assertRaises(lnutil.PaymentFailure): with self.assertRaises(lnutil.PaymentFailure):
self.alice_channel.add_htlc(htlc_dict) self.alice_channel.add_htlc(htlc)
with self.assertRaises(lnutil.RemoteMisbehaving): with self.assertRaises(lnutil.RemoteMisbehaving):
self.bob_channel.receive_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc)
def part3(self): def part3(self):
# Add a HTLC of 2 BTC to Alice, and the settle it. # Add a HTLC of 2 BTC to Alice, and the settle it.
@@ -885,14 +900,14 @@ class TestChanReserve(ElectrumTestCase):
# Bob: 7.0 # Bob: 7.0
paymentPreimage = b"\x01" * 32 paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage) paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = { htlc = UpdateAddHtlc(
'payment_hash': paymentHash, payment_hash=paymentHash,
'amount_msat': int(2 * one_bitcoin_in_msat), amount_msat=int(2 * one_bitcoin_in_msat),
'cltv_abs': 5, cltv_abs=5,
'timestamp': 0, timestamp=0,
} )
alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id alice_idx = self.alice_channel.add_htlc(htlc).htlc_id
bob_idx = self.bob_channel.receive_htlc(htlc_dict).htlc_id bob_idx = self.bob_channel.receive_htlc(htlc).htlc_id
force_state_transition(self.alice_channel, self.bob_channel) force_state_transition(self.alice_channel, self.bob_channel)
self.check_bals(one_bitcoin_in_msat * 3 self.check_bals(one_bitcoin_in_msat * 3
- self.alice_channel.get_next_fee(LOCAL), - self.alice_channel.get_next_fee(LOCAL),
@@ -906,9 +921,9 @@ class TestChanReserve(ElectrumTestCase):
# And now let Bob add an HTLC of 1 BTC. This will take Bob's balance # And now let Bob add an HTLC of 1 BTC. This will take Bob's balance
# all the way down to his channel reserve, but since he is not paying # all the way down to his channel reserve, but since he is not paying
# the fee this is okay. # the fee this is okay.
htlc_dict['amount_msat'] = one_bitcoin_in_msat htlc = dataclasses.replace(htlc, amount_msat=one_bitcoin_in_msat)
self.bob_channel.add_htlc(htlc_dict) self.bob_channel.add_htlc(htlc)
self.alice_channel.receive_htlc(htlc_dict) self.alice_channel.receive_htlc(htlc)
force_state_transition(self.alice_channel, self.bob_channel) force_state_transition(self.alice_channel, self.bob_channel)
self.check_bals(one_bitcoin_in_msat * 3 \ self.check_bals(one_bitcoin_in_msat * 3 \
- self.alice_channel.get_next_fee(LOCAL), - self.alice_channel.get_next_fee(LOCAL),
@@ -943,12 +958,12 @@ class TestDust(ElectrumTestCase):
# to pay for his htlc success transaction # to pay for his htlc success transaction
below_dust_for_bob = dust_limit_bob - 1 below_dust_for_bob = dust_limit_bob - 1
htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000) htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000)
htlc = { htlc = UpdateAddHtlc(
'payment_hash': paymentHash, payment_hash=paymentHash,
'amount_msat': 1000 * htlc_amt, amount_msat=1000 * htlc_amt,
'cltv_abs': 5, # consistent with channel policy cltv_abs=5, # consistent with channel policy
'timestamp': 0, timestamp=0,
} )
# add the htlc # add the htlc
alice_htlc_id = alice_channel.add_htlc(htlc).htlc_id alice_htlc_id = alice_channel.add_htlc(htlc).htlc_id