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:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user