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,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import dataclasses
import enum
from collections import defaultdict
from enum import IntEnum, Enum
@@ -1202,12 +1203,10 @@ class Channel(AbstractChannel):
"""Adds a new LOCAL HTLC to the channel.
Action must be initiated by LOCAL.
"""
if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc)
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat)
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:
self.hm.send_htlc(htlc)
self.logger.info("add_htlc")
@@ -1217,15 +1216,13 @@ class Channel(AbstractChannel):
"""Adds a new REMOTE HTLC to the channel.
Action must be initiated by REMOTE.
"""
if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc)
try:
self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat)
except PaymentFailure as e:
raise RemoteMisbehaving(e) from e
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:
self.hm.recv_htlc(htlc)
if onion_packet:

View File

@@ -12,9 +12,10 @@ from functools import lru_cache
import electrum_ecc as ecc
from electrum_ecc import CURVE_ORDER, ecdsa_sig64_from_der_sig
from electrum_ecc.util import bip340_tagged_hash
import dataclasses
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 .crypto import sha256, pw_decode_with_version_and_mac
@@ -22,7 +23,8 @@ from .transaction import (
Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, OPPushDataPubkey
)
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 .bip32 import BIP32Node, BIP32_PRIME
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
@attr.s(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class UpdateAddHtlc:
amount_msat = attr.ib(type=int, kw_only=True)
payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes, repr=lambda val: val.hex())
cltv_abs = attr.ib(type=int, kw_only=True)
timestamp = attr.ib(type=int, kw_only=True)
htlc_id = attr.ib(type=int, kw_only=True, default=None)
amount_msat: int
payment_hash: bytes
cltv_abs: int
htlc_id: Optional[int] = dataclasses.field(default=None)
timestamp: int = dataclasses.field(default_factory=lambda: int(time.time()))
@staticmethod
@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(
amount_msat=amount_msat,
payment_hash=payment_hash,
payment_hash=bytes.fromhex(rhash),
cltv_abs=cltv_abs,
htlc_id=htlc_id,
timestamp=timestamp)
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):

View File

@@ -27,6 +27,7 @@ import os
import binascii
from pprint import pformat
import logging
import dataclasses
from electrum import bitcoin
from electrum import lnpeer
@@ -257,31 +258,34 @@ class TestChannel(ElectrumTestCase):
self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage)
self.htlc_dict = {
'payment_hash': paymentHash,
'amount_msat': one_bitcoin_in_msat,
'cltv_abs': 5,
'timestamp': 0,
}
self.htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=one_bitcoin_in_msat,
cltv_abs=5,
timestamp=0,
)
# 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
# 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()), [])
before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE)
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]
def test_concurrent_reversed_payment(self):
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
self.htlc_dict['amount_msat'] += 1000
self.bob_channel.add_htlc(self.htlc_dict)
self.alice_channel.receive_htlc(self.htlc_dict)
self.htlc = dataclasses.replace(
self.htlc,
payment_hash=bitcoin.sha256(32 * b'\x02'),
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(3, self.alice_channel.get_next_commitment(LOCAL))
@@ -561,9 +565,12 @@ class TestChannel(ElectrumTestCase):
tx6 = str(alice_channel.force_close_tx())
self.assertNotEqual(tx5, tx6)
self.htlc_dict['amount_msat'] *= 5
bob_index = bob_channel.add_htlc(self.htlc_dict).htlc_id
alice_index = alice_channel.receive_htlc(self.htlc_dict).htlc_id
self.htlc = dataclasses.replace(
self.htlc,
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)
@@ -662,18 +669,26 @@ class TestChannel(ElectrumTestCase):
self.alice_to_bob_fee_update(0)
force_state_transition(self.alice_channel, self.bob_channel)
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
self.alice_channel.add_htlc(self.htlc_dict)
self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03')
self.alice_channel.add_htlc(self.htlc_dict)
self.htlc = dataclasses.replace(
self.htlc,
payment_hash=bitcoin.sha256(32 * b'\x02'),
)
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)
# 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
# has to pay a commitment fee).
new = dict(self.htlc_dict)
new['amount_msat'] *= 2.5
new['payment_hash'] = bitcoin.sha256(32 * b'\x04')
new = dataclasses.replace(
self.htlc,
amount_msat=int(self.htlc.amount_msat * 2.5),
payment_hash=bitcoin.sha256(32 * b'\x04'),
)
with self.assertRaises(lnutil.PaymentFailure) as cm:
self.alice_channel.add_htlc(new)
self.assertIn('Not enough local balance', cm.exception.args[0])
@@ -822,14 +837,14 @@ class TestChanReserve(ElectrumTestCase):
# Bob: 5.0
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = {
'payment_hash': paymentHash,
'amount_msat': int(.5 * one_bitcoin_in_msat),
'cltv_abs': 5,
'timestamp': 0,
}
self.alice_channel.add_htlc(htlc_dict)
self.bob_channel.receive_htlc(htlc_dict)
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=int(.5 * one_bitcoin_in_msat),
cltv_abs=5,
timestamp=0,
)
self.alice_channel.add_htlc(htlc)
self.bob_channel.receive_htlc(htlc)
# Force a state transition, making sure this HTLC is considered valid
# even though the channel reserves are not met.
force_state_transition(self.alice_channel, self.bob_channel)
@@ -847,10 +862,10 @@ class TestChanReserve(ElectrumTestCase):
# Alice: 4.5
# Bob: 5.0
with self.assertRaises(lnutil.PaymentFailure):
htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
self.bob_channel.add_htlc(htlc_dict)
htlc = dataclasses.replace(htlc, payment_hash=bitcoin.sha256(32 * b'\x02'))
self.bob_channel.add_htlc(htlc)
with self.assertRaises(lnutil.RemoteMisbehaving):
self.alice_channel.receive_htlc(htlc_dict)
self.alice_channel.receive_htlc(htlc)
def part2(self):
paymentPreimage = b"\x01" * 32
@@ -861,22 +876,22 @@ class TestChanReserve(ElectrumTestCase):
# Resulting balances:
# Alice: 1.5
# Bob: 9.5
htlc_dict = {
'payment_hash': paymentHash,
'amount_msat': int(3.5 * one_bitcoin_in_msat),
'cltv_abs': 5,
}
self.alice_channel.add_htlc(htlc_dict)
self.bob_channel.receive_htlc(htlc_dict)
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=int(3.5 * one_bitcoin_in_msat),
cltv_abs=5,
)
self.alice_channel.add_htlc(htlc)
self.bob_channel.receive_htlc(htlc)
# 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
# she is the initiator the additional transaction fee makes her
# 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):
self.alice_channel.add_htlc(htlc_dict)
self.alice_channel.add_htlc(htlc)
with self.assertRaises(lnutil.RemoteMisbehaving):
self.bob_channel.receive_htlc(htlc_dict)
self.bob_channel.receive_htlc(htlc)
def part3(self):
# Add a HTLC of 2 BTC to Alice, and the settle it.
@@ -885,14 +900,14 @@ class TestChanReserve(ElectrumTestCase):
# Bob: 7.0
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = {
'payment_hash': paymentHash,
'amount_msat': int(2 * one_bitcoin_in_msat),
'cltv_abs': 5,
'timestamp': 0,
}
alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id
bob_idx = self.bob_channel.receive_htlc(htlc_dict).htlc_id
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=int(2 * one_bitcoin_in_msat),
cltv_abs=5,
timestamp=0,
)
alice_idx = self.alice_channel.add_htlc(htlc).htlc_id
bob_idx = self.bob_channel.receive_htlc(htlc).htlc_id
force_state_transition(self.alice_channel, self.bob_channel)
self.check_bals(one_bitcoin_in_msat * 3
- 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
# all the way down to his channel reserve, but since he is not paying
# the fee this is okay.
htlc_dict['amount_msat'] = one_bitcoin_in_msat
self.bob_channel.add_htlc(htlc_dict)
self.alice_channel.receive_htlc(htlc_dict)
htlc = dataclasses.replace(htlc, amount_msat=one_bitcoin_in_msat)
self.bob_channel.add_htlc(htlc)
self.alice_channel.receive_htlc(htlc)
force_state_transition(self.alice_channel, self.bob_channel)
self.check_bals(one_bitcoin_in_msat * 3 \
- self.alice_channel.get_next_fee(LOCAL),
@@ -943,12 +958,12 @@ class TestDust(ElectrumTestCase):
# to pay for his htlc success transaction
below_dust_for_bob = dust_limit_bob - 1
htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000)
htlc = {
'payment_hash': paymentHash,
'amount_msat': 1000 * htlc_amt,
'cltv_abs': 5, # consistent with channel policy
'timestamp': 0,
}
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=1000 * htlc_amt,
cltv_abs=5, # consistent with channel policy
timestamp=0,
)
# add the htlc
alice_htlc_id = alice_channel.add_htlc(htlc).htlc_id