mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
lnchan refactor
- replace undoing logic with new HTLCManager class - separate SENT/RECEIVED - move UpdateAddHtlc to lnutil
This commit is contained in:
parent
ef88bb1c28
commit
e56e849505
11 changed files with 706 additions and 398 deletions
|
@ -5,9 +5,9 @@ import PyQt5.QtWidgets as QtWidgets
|
|||
import PyQt5.QtCore as QtCore
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.lnchan import UpdateAddHtlc, HTLCOwner
|
||||
from electrum.util import bh2u, format_time
|
||||
from electrum.lnutil import format_short_channel_id, SENT, RECEIVED
|
||||
from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction
|
||||
from electrum.lnchan import htlcsum
|
||||
from electrum.lnaddr import LnAddr, lndecode
|
||||
from electrum.bitcoin import COIN
|
||||
|
||||
|
@ -30,8 +30,8 @@ class LinkedLabel(QtWidgets.QLabel):
|
|||
self.linkActivated.connect(on_clicked)
|
||||
|
||||
class ChannelDetailsDialog(QtWidgets.QDialog):
|
||||
def make_htlc_item(self, i: UpdateAddHtlc, direction: HTLCOwner) -> HTLCItem:
|
||||
it = HTLCItem(_('Sent HTLC with ID {}' if SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id))
|
||||
def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem:
|
||||
it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id))
|
||||
it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))])
|
||||
it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))])
|
||||
it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))])
|
||||
|
@ -45,7 +45,7 @@ class ChannelDetailsDialog(QtWidgets.QDialog):
|
|||
invoice.appendRow([HTLCItem(_('Date')), HTLCItem(format_time(lnaddr.date))])
|
||||
it.appendRow([invoice])
|
||||
|
||||
def make_inflight(self, lnaddr, i: UpdateAddHtlc, direction: HTLCOwner) -> HTLCItem:
|
||||
def make_inflight(self, lnaddr, i: UpdateAddHtlc, direction: Direction) -> HTLCItem:
|
||||
it = self.make_htlc_item(i, direction)
|
||||
self.append_lnaddr(it, lnaddr)
|
||||
return it
|
||||
|
@ -99,23 +99,23 @@ class ChannelDetailsDialog(QtWidgets.QDialog):
|
|||
dest_mapping = self.keyname_rows[to]
|
||||
dest_mapping[payment_hash] = len(dest_mapping)
|
||||
|
||||
ln_payment_completed = QtCore.pyqtSignal(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
|
||||
htlc_added = QtCore.pyqtSignal(str, UpdateAddHtlc, LnAddr, HTLCOwner)
|
||||
ln_payment_completed = QtCore.pyqtSignal(str, float, Direction, UpdateAddHtlc, bytes, bytes)
|
||||
htlc_added = QtCore.pyqtSignal(str, UpdateAddHtlc, LnAddr, Direction)
|
||||
|
||||
@QtCore.pyqtSlot(str, UpdateAddHtlc, LnAddr, HTLCOwner)
|
||||
@QtCore.pyqtSlot(str, UpdateAddHtlc, LnAddr, Direction)
|
||||
def do_htlc_added(self, evtname, htlc, lnaddr, direction):
|
||||
mapping = self.keyname_rows['inflight']
|
||||
mapping[htlc.payment_hash] = len(mapping)
|
||||
self.folders['inflight'].appendRow(self.make_inflight(lnaddr, htlc, direction))
|
||||
|
||||
@QtCore.pyqtSlot(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
|
||||
@QtCore.pyqtSlot(str, float, Direction, UpdateAddHtlc, bytes, bytes)
|
||||
def do_ln_payment_completed(self, evtname, date, direction, htlc, preimage, chan_id):
|
||||
self.move('inflight', 'settled', htlc.payment_hash)
|
||||
self.update_sent_received()
|
||||
|
||||
def update_sent_received(self):
|
||||
self.sent_label.setText(str(sum(self.chan.settled[SENT])))
|
||||
self.received_label.setText(str(sum(self.chan.settled[RECEIVED])))
|
||||
self.sent_label.setText(str(htlcsum(self.hm.settled_htlcs_by(LOCAL))))
|
||||
self.received_label.setText(str(htlcsum(self.hm.settled_htlcs_by(REMOTE))))
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def show_tx(self, link_text: str):
|
||||
|
|
|
@ -30,8 +30,9 @@ class ChannelsList(MyTreeView):
|
|||
for subject in (REMOTE, LOCAL):
|
||||
bal_minus_htlcs = chan.balance_minus_outgoing_htlcs(subject)//1000
|
||||
label = self.parent.format_amount(bal_minus_htlcs)
|
||||
bal_other = chan.balance(-subject)//1000
|
||||
bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(-subject)//1000
|
||||
other = subject.inverted()
|
||||
bal_other = chan.balance(other)//1000
|
||||
bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(other)//1000
|
||||
if bal_other != bal_minus_htlcs_other:
|
||||
label += ' (+' + self.parent.format_amount(bal_other - bal_minus_htlcs_other) + ')'
|
||||
labels[subject] = label
|
||||
|
|
|
@ -25,8 +25,8 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabl
|
|||
from .transaction import Transaction, TxOutput
|
||||
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
|
||||
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage)
|
||||
from .lnchan import Channel, RevokeAndAck, htlcsum, UpdateAddHtlc
|
||||
from .lnutil import (Outpoint, LocalConfig, RECEIVED,
|
||||
from .lnchan import Channel, RevokeAndAck, htlcsum
|
||||
from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
|
||||
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
|
||||
funding_output_script, get_per_commitment_secret_from_seed,
|
||||
secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures,
|
||||
|
@ -397,20 +397,20 @@ class Peer(PrintError):
|
|||
htlc_basepoint=keypair_generator(LnKeyFamily.HTLC_BASE),
|
||||
delayed_basepoint=keypair_generator(LnKeyFamily.DELAY_BASE),
|
||||
revocation_basepoint=keypair_generator(LnKeyFamily.REVOCATION_BASE),
|
||||
to_self_delay=143,
|
||||
to_self_delay=9,
|
||||
dust_limit_sat=546,
|
||||
max_htlc_value_in_flight_msat=0xffffffffffffffff,
|
||||
max_accepted_htlcs=5,
|
||||
initial_msat=initial_msat,
|
||||
ctn=-1,
|
||||
next_htlc_id=0,
|
||||
amount_msat=initial_msat,
|
||||
reserve_sat=546,
|
||||
per_commitment_secret_seed=keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey,
|
||||
funding_locked_received=False,
|
||||
was_announced=False,
|
||||
current_commitment_signature=None,
|
||||
current_htlc_signatures=[],
|
||||
got_sig_for_next=False,
|
||||
)
|
||||
return local_config
|
||||
|
||||
|
@ -472,7 +472,6 @@ class Peer(PrintError):
|
|||
max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'),
|
||||
initial_msat=push_msat,
|
||||
ctn = -1,
|
||||
amount_msat=push_msat,
|
||||
next_htlc_id = 0,
|
||||
reserve_sat = remote_reserve_sat,
|
||||
|
||||
|
@ -517,9 +516,11 @@ class Peer(PrintError):
|
|||
# broadcast funding tx
|
||||
await self.network.broadcast_transaction(funding_tx)
|
||||
chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE)
|
||||
chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0)
|
||||
chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig)
|
||||
chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=remote_per_commitment_point, next_per_commitment_point=None)
|
||||
chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig, got_sig_for_next=False)
|
||||
chan.set_state('OPENING')
|
||||
chan.set_remote_commitment()
|
||||
chan.set_local_commitment(chan.current_commitment(LOCAL))
|
||||
return chan
|
||||
|
||||
async def on_open_channel(self, payload):
|
||||
|
@ -579,7 +580,6 @@ class Peer(PrintError):
|
|||
max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'),
|
||||
initial_msat=remote_balance_sat,
|
||||
ctn = -1,
|
||||
amount_msat=remote_balance_sat,
|
||||
next_htlc_id = 0,
|
||||
reserve_sat = remote_reserve_sat,
|
||||
|
||||
|
@ -605,7 +605,7 @@ class Peer(PrintError):
|
|||
)
|
||||
chan.set_state('OPENING')
|
||||
chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE)
|
||||
chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0)
|
||||
chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=payload['first_per_commitment_point'], next_per_commitment_point=None)
|
||||
chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig)
|
||||
self.lnworker.save_channel(chan)
|
||||
self.lnwatcher.watch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str())
|
||||
|
@ -732,7 +732,7 @@ class Peer(PrintError):
|
|||
if not chan.config[LOCAL].funding_locked_received:
|
||||
our_next_point = chan.config[REMOTE].next_per_commitment_point
|
||||
their_next_point = payload["next_per_commitment_point"]
|
||||
new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point, current_per_commitment_point=our_next_point)
|
||||
new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point)
|
||||
new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True)
|
||||
chan.config[REMOTE]=new_remote_state
|
||||
chan.config[LOCAL]=new_local_state
|
||||
|
|
|
@ -27,24 +27,25 @@ import binascii
|
|||
import json
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
|
||||
from . import ecc
|
||||
from .util import bfh, PrintError, bh2u
|
||||
from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
|
||||
from .bitcoin import redeem_script_to_address
|
||||
from .crypto import sha256, sha256d
|
||||
from . import ecc
|
||||
from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
|
||||
from .lnutil import get_per_commitment_secret_from_seed
|
||||
from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey
|
||||
from .lnutil import sign_and_get_sig_string
|
||||
from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
|
||||
from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
|
||||
from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_commitment_outputs
|
||||
from .lnutil import ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script
|
||||
from .simple_config import get_config
|
||||
from .transaction import Transaction
|
||||
|
||||
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
|
||||
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
|
||||
sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey,
|
||||
make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc,
|
||||
HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc,
|
||||
funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,
|
||||
ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script)
|
||||
from .lnsweep import create_sweeptxs_for_their_just_revoked_ctx
|
||||
from .lnsweep import create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx
|
||||
from .lnhtlc import HTLCManager
|
||||
|
||||
|
||||
class ChannelJsonEncoder(json.JSONEncoder):
|
||||
|
@ -83,22 +84,6 @@ class FeeUpdate(defaultdict):
|
|||
return self.rate
|
||||
# implicit return None
|
||||
|
||||
class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'htlc_id'])):
|
||||
"""
|
||||
This whole class body is so that if you pass a hex-string as payment_hash,
|
||||
it is decoded to bytes. Bytes can't be saved to disk, so we save hex-strings.
|
||||
"""
|
||||
__slots__ = ()
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
if type(args[1]) is str:
|
||||
args[1] = bfh(args[1])
|
||||
return super().__new__(cls, *args)
|
||||
if type(kwargs['payment_hash']) is str:
|
||||
kwargs['payment_hash'] = bfh(kwargs['payment_hash'])
|
||||
return super().__new__(cls, **kwargs)
|
||||
|
||||
def decodeAll(d, local):
|
||||
for k, v in d.items():
|
||||
if k == 'revocation_store':
|
||||
|
@ -124,20 +109,6 @@ def str_bytes_dict_from_save(x):
|
|||
def str_bytes_dict_to_save(x):
|
||||
return {str(k): bh2u(v) for k, v in x.items()}
|
||||
|
||||
class HtlcChanges(NamedTuple):
|
||||
# ints are htlc ids
|
||||
adds: Dict[int, UpdateAddHtlc]
|
||||
settles: Set[int]
|
||||
fails: Set[int]
|
||||
locked_in: Set[int]
|
||||
|
||||
@staticmethod
|
||||
def new():
|
||||
"""
|
||||
Since we can't use default arguments for these types (they would be shared among instances)
|
||||
"""
|
||||
return HtlcChanges({}, set(), set(), set())
|
||||
|
||||
class Channel(PrintError):
|
||||
def diagnostic_name(self):
|
||||
if self.name:
|
||||
|
@ -147,7 +118,7 @@ class Channel(PrintError):
|
|||
except:
|
||||
return super().diagnostic_name()
|
||||
|
||||
def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None):
|
||||
def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[Direction, UpdateAddHtlc, bytes], None]] = None):
|
||||
self.preimages = {}
|
||||
if not payment_completed:
|
||||
payment_completed = lambda this, x, y, z: None
|
||||
|
@ -179,13 +150,9 @@ class Channel(PrintError):
|
|||
# we should not persist txns in this format. we should persist htlcs, and be able to derive
|
||||
# any past commitment transaction and use that instead; until then...
|
||||
self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"])
|
||||
self.remote_commitment_to_be_revoked.deserialize(True)
|
||||
|
||||
self.log = {LOCAL: HtlcChanges.new(), REMOTE: HtlcChanges.new()}
|
||||
for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
|
||||
if strname not in state: continue
|
||||
for y in state[strname]:
|
||||
htlc = UpdateAddHtlc(**y)
|
||||
self.log[subject].adds[htlc.htlc_id] = htlc
|
||||
self.hm = HTLCManager(state.get('log'))
|
||||
|
||||
self.name = name
|
||||
|
||||
|
@ -200,23 +167,18 @@ class Channel(PrintError):
|
|||
|
||||
self.lnwatcher = None
|
||||
|
||||
self.settled = {LOCAL: state.get('settled_local', []), REMOTE: state.get('settled_remote', [])}
|
||||
|
||||
for sub in (LOCAL, REMOTE):
|
||||
self.log[sub].locked_in.update(self.log[sub].adds.keys())
|
||||
|
||||
self.set_local_commitment(self.current_commitment(LOCAL))
|
||||
self.set_remote_commitment(self.current_commitment(REMOTE))
|
||||
self.local_commitment = None
|
||||
self.remote_commitment = None
|
||||
|
||||
def set_local_commitment(self, ctx):
|
||||
ctn = extract_ctn_from_tx_and_chan(ctx, self)
|
||||
assert self.signature_fits(ctx), (self.log[LOCAL])
|
||||
self.local_commitment = ctx
|
||||
if self.sweep_address is not None:
|
||||
self.local_sweeptxs = create_sweeptxs_for_our_latest_ctx(self, self.local_commitment, self.sweep_address)
|
||||
|
||||
self.assert_signature_fits(ctx)
|
||||
|
||||
def set_remote_commitment(self, ctx):
|
||||
self.remote_commitment = ctx
|
||||
def set_remote_commitment(self):
|
||||
self.remote_commitment = self.current_commitment(REMOTE)
|
||||
if self.sweep_address is not None:
|
||||
self.remote_sweeptxs = create_sweeptxs_for_their_latest_ctx(self, self.remote_commitment, self.sweep_address)
|
||||
|
||||
|
@ -233,9 +195,9 @@ class Channel(PrintError):
|
|||
raise PaymentFailure('Channel not open')
|
||||
if self.available_to_spend(LOCAL) < amount_msat:
|
||||
raise PaymentFailure(f'Not enough local balance. Have: {self.available_to_spend(LOCAL)}, Need: {amount_msat}')
|
||||
if len(self.htlcs(LOCAL, only_pending=True)) + 1 > self.config[REMOTE].max_accepted_htlcs:
|
||||
if len(self.hm.htlcs(LOCAL)) + 1 > self.config[REMOTE].max_accepted_htlcs:
|
||||
raise PaymentFailure('Too many HTLCs already in channel')
|
||||
current_htlc_sum = htlcsum(self.htlcs(LOCAL, only_pending=True))
|
||||
current_htlc_sum = htlcsum(self.hm.htlcs_by_direction(LOCAL, SENT)) + htlcsum(self.hm.htlcs_by_direction(LOCAL, RECEIVED))
|
||||
if current_htlc_sum + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
|
||||
raise PaymentFailure(f'HTLC value sum (sum of pending htlcs: {current_htlc_sum/1000} sat plus new htlc: {amount_msat/1000} sat) would exceed max allowed: {self.config[REMOTE].max_htlc_value_in_flight_msat/1000} sat')
|
||||
if amount_msat <= 0: # FIXME htlc_minimum_msat
|
||||
|
@ -269,7 +231,7 @@ class Channel(PrintError):
|
|||
assert type(htlc) is dict
|
||||
self._check_can_pay(htlc['amount_msat'])
|
||||
htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id)
|
||||
self.log[LOCAL].adds[htlc.htlc_id] = htlc
|
||||
self.hm.send_htlc(htlc)
|
||||
self.print_error("add_htlc")
|
||||
self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1)
|
||||
return htlc.htlc_id
|
||||
|
@ -288,8 +250,7 @@ class Channel(PrintError):
|
|||
raise RemoteMisbehaving('Remote dipped below channel reserve.' +\
|
||||
f' Available at remote: {self.available_to_spend(REMOTE)},' +\
|
||||
f' HTLC amount: {htlc.amount_msat}')
|
||||
adds = self.log[REMOTE].adds
|
||||
adds[htlc.htlc_id] = htlc
|
||||
self.hm.recv_htlc(htlc)
|
||||
self.print_error("receive_htlc")
|
||||
self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1)
|
||||
return htlc.htlc_id
|
||||
|
@ -308,7 +269,7 @@ class Channel(PrintError):
|
|||
"""
|
||||
self.print_error("sign_next_commitment")
|
||||
|
||||
old_logs = dict(self.lock_in_htlc_changes(LOCAL))
|
||||
self.hm.send_ctx()
|
||||
|
||||
pending_remote_commitment = self.pending_commitment(REMOTE)
|
||||
sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
|
||||
|
@ -321,7 +282,8 @@ class Channel(PrintError):
|
|||
for_us = False
|
||||
|
||||
htlcsigs = []
|
||||
for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]):
|
||||
# they sent => we receive
|
||||
for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, SENT, ctn=self.config[REMOTE].ctn+1), self.included_htlcs(REMOTE, RECEIVED, ctn=self.config[REMOTE].ctn+1)]):
|
||||
for htlc in htlcs:
|
||||
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
|
||||
pcp=self.config[REMOTE].next_per_commitment_point,
|
||||
|
@ -337,26 +299,11 @@ class Channel(PrintError):
|
|||
htlcsigs.sort()
|
||||
htlcsigs = [x[1] for x in htlcsigs]
|
||||
|
||||
self.remote_commitment = self.pending_commitment(REMOTE)
|
||||
|
||||
# we can't know if this message arrives.
|
||||
# since we shouldn't actually throw away
|
||||
# failed htlcs yet (or mark htlc locked in),
|
||||
# roll back the changes that were made
|
||||
self.log = old_logs
|
||||
# TODO should add remote_commitment here and handle
|
||||
# both valid ctx'es in lnwatcher at the same time...
|
||||
|
||||
return sig_64, htlcsigs
|
||||
|
||||
def lock_in_htlc_changes(self, subject):
|
||||
for sub in (LOCAL, REMOTE):
|
||||
log = self.log[sub]
|
||||
yield (sub, deepcopy(log))
|
||||
for htlc_id in log.fails:
|
||||
log.adds.pop(htlc_id)
|
||||
log.fails.clear()
|
||||
|
||||
self.log[subject].locked_in.update(self.log[subject].adds.keys())
|
||||
|
||||
def receive_new_commitment(self, sig, htlc_sigs):
|
||||
"""
|
||||
ReceiveNewCommitment process a signature for a new commitment state sent by
|
||||
|
@ -372,7 +319,7 @@ class Channel(PrintError):
|
|||
"""
|
||||
self.print_error("receive_new_commitment")
|
||||
|
||||
for _ in self.lock_in_htlc_changes(REMOTE): pass
|
||||
self.hm.recv_ctx()
|
||||
|
||||
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
|
||||
|
||||
|
@ -385,16 +332,18 @@ class Channel(PrintError):
|
|||
htlc_sigs_string = b''.join(htlc_sigs)
|
||||
|
||||
htlc_sigs = htlc_sigs[:] # copy cause we will delete now
|
||||
for htlcs, we_receive in [(self.included_htlcs(LOCAL, REMOTE), True), (self.included_htlcs(LOCAL, LOCAL), False)]:
|
||||
ctn = self.config[LOCAL].ctn+1
|
||||
for htlcs, we_receive in [(self.included_htlcs(LOCAL, SENT, ctn=ctn), False), (self.included_htlcs(LOCAL, RECEIVED, ctn=ctn), True)]:
|
||||
for htlc in htlcs:
|
||||
idx = self.verify_htlc(htlc, htlc_sigs, we_receive)
|
||||
idx = self.verify_htlc(htlc, htlc_sigs, we_receive, pending_local_commitment)
|
||||
del htlc_sigs[idx]
|
||||
if len(htlc_sigs) != 0: # all sigs should have been popped above
|
||||
raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures')
|
||||
|
||||
self.config[LOCAL]=self.config[LOCAL]._replace(
|
||||
current_commitment_signature=sig,
|
||||
current_htlc_signatures=htlc_sigs_string)
|
||||
current_htlc_signatures=htlc_sigs_string,
|
||||
got_sig_for_next=True)
|
||||
|
||||
if self.pending_fee is not None:
|
||||
if not self.constraints.is_initiator:
|
||||
|
@ -402,15 +351,15 @@ class Channel(PrintError):
|
|||
if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]:
|
||||
self.pending_fee[FUNDER_SIGNED] = True
|
||||
|
||||
self.set_local_commitment(self.pending_commitment(LOCAL))
|
||||
self.set_local_commitment(pending_local_commitment)
|
||||
|
||||
def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool) -> int:
|
||||
_, this_point, _ = self.points()
|
||||
def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool, ctx) -> int:
|
||||
_, this_point, _, _ = self.points()
|
||||
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
|
||||
pcp=this_point,
|
||||
for_us=True,
|
||||
we_receive=we_receive,
|
||||
commit=self.pending_commitment(LOCAL),
|
||||
commit=ctx,
|
||||
htlc=htlc)
|
||||
pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0)))
|
||||
remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point)
|
||||
|
@ -418,19 +367,19 @@ class Channel(PrintError):
|
|||
if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
|
||||
return idx
|
||||
else:
|
||||
raise Exception(f'failed verifying HTLC signatures: {htlc}')
|
||||
raise Exception(f'failed verifying HTLC signatures: {htlc}, sigs: {len(htlc_sigs)}, we_receive: {we_receive}')
|
||||
|
||||
def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool) -> bytes:
|
||||
def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool, ctx) -> bytes:
|
||||
data = self.config[LOCAL].current_htlc_signatures
|
||||
htlc_sigs = [data[i:i + 64] for i in range(0, len(data), 64)]
|
||||
idx = self.verify_htlc(htlc, htlc_sigs, we_receive=we_receive)
|
||||
idx = self.verify_htlc(htlc, htlc_sigs, we_receive=we_receive, ctx=ctx)
|
||||
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01'
|
||||
return remote_htlc_sig
|
||||
|
||||
def revoke_current_commitment(self):
|
||||
self.print_error("revoke_current_commitment")
|
||||
|
||||
last_secret, this_point, next_point = self.points()
|
||||
last_secret, this_point, next_point, _ = self.points()
|
||||
|
||||
new_feerate = self.constraints.feerate
|
||||
|
||||
|
@ -444,16 +393,18 @@ class Channel(PrintError):
|
|||
self.pending_fee = None
|
||||
print("FEERATE CHANGE COMPLETE (initiator)")
|
||||
|
||||
self.config[LOCAL]=self.config[LOCAL]._replace(
|
||||
ctn=self.config[LOCAL].ctn + 1,
|
||||
)
|
||||
assert self.config[LOCAL].got_sig_for_next
|
||||
self.constraints=self.constraints._replace(
|
||||
feerate=new_feerate
|
||||
)
|
||||
|
||||
# since we should not revoke our latest commitment tx,
|
||||
# we do not update self.local_commitment here,
|
||||
# it should instead be updated when we receive a new sig
|
||||
self.set_local_commitment(self.pending_commitment(LOCAL))
|
||||
ctx = self.pending_commitment(LOCAL)
|
||||
self.hm.send_rev()
|
||||
self.config[LOCAL]=self.config[LOCAL]._replace(
|
||||
ctn=self.config[LOCAL].ctn + 1,
|
||||
got_sig_for_next=False,
|
||||
)
|
||||
assert self.signature_fits(ctx)
|
||||
|
||||
return RevokeAndAck(last_secret, next_point), "current htlcs"
|
||||
|
||||
|
@ -466,7 +417,8 @@ class Channel(PrintError):
|
|||
this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big'))
|
||||
next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num)
|
||||
next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big'))
|
||||
return last_secret, this_point, next_point
|
||||
last_point = secret_to_pubkey(int.from_bytes(last_secret, 'big'))
|
||||
return last_secret, this_point, next_point, last_point
|
||||
|
||||
def process_new_revocation_secret(self, per_commitment_secret: bytes):
|
||||
if not self.lnwatcher:
|
||||
|
@ -481,12 +433,9 @@ class Channel(PrintError):
|
|||
def receive_revocation(self, revocation) -> Tuple[int, int]:
|
||||
self.print_error("receive_revocation")
|
||||
|
||||
old_logs = dict(self.lock_in_htlc_changes(LOCAL))
|
||||
|
||||
cur_point = self.config[REMOTE].current_per_commitment_point
|
||||
derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True)
|
||||
if cur_point != derived_point:
|
||||
self.log = old_logs
|
||||
raise Exception('revoked secret not for current point')
|
||||
|
||||
# FIXME not sure this is correct... but it seems to work
|
||||
|
@ -505,51 +454,36 @@ class Channel(PrintError):
|
|||
if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]:
|
||||
self.pending_fee[FUNDER_SIGNED] = True
|
||||
|
||||
def mark_settled(subject):
|
||||
"""
|
||||
find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs
|
||||
"""
|
||||
old_amount = htlcsum(self.htlcs(subject, False))
|
||||
|
||||
for htlc_id in self.log[subject].settles:
|
||||
adds = self.log[subject].adds
|
||||
htlc = adds.pop(htlc_id)
|
||||
self.settled[subject].append(htlc.amount_msat)
|
||||
if subject == LOCAL:
|
||||
preimage = self.preimages.pop(htlc_id)
|
||||
else:
|
||||
preimage = None
|
||||
self.payment_completed(self, subject, htlc, preimage)
|
||||
self.log[subject].settles.clear()
|
||||
|
||||
return old_amount - htlcsum(self.htlcs(subject, False))
|
||||
|
||||
sent_this_batch = mark_settled(LOCAL)
|
||||
received_this_batch = mark_settled(REMOTE)
|
||||
received = self.hm.received_in_ctn(self.config[REMOTE].ctn + 1)
|
||||
sent = self.hm.sent_in_ctn(self.config[REMOTE].ctn + 1)
|
||||
for htlc in received:
|
||||
self.payment_completed(self, RECEIVED, htlc, None)
|
||||
for htlc in sent:
|
||||
preimage = self.preimages.pop(htlc.htlc_id)
|
||||
self.payment_completed(self, SENT, htlc, preimage)
|
||||
received_this_batch = htlcsum(received)
|
||||
sent_this_batch = htlcsum(sent)
|
||||
|
||||
next_point = self.config[REMOTE].next_per_commitment_point
|
||||
|
||||
print("RECEIVED", received_this_batch)
|
||||
print("SENT", sent_this_batch)
|
||||
self.hm.recv_rev()
|
||||
|
||||
self.config[REMOTE]=self.config[REMOTE]._replace(
|
||||
ctn=self.config[REMOTE].ctn + 1,
|
||||
current_per_commitment_point=next_point,
|
||||
next_per_commitment_point=revocation.next_per_commitment_point,
|
||||
amount_msat=self.config[REMOTE].amount_msat + (sent_this_batch - received_this_batch)
|
||||
)
|
||||
self.config[LOCAL]=self.config[LOCAL]._replace(
|
||||
amount_msat = self.config[LOCAL].amount_msat + (received_this_batch - sent_this_batch)
|
||||
)
|
||||
|
||||
if self.pending_fee is not None:
|
||||
if self.constraints.is_initiator:
|
||||
self.pending_fee[FUNDEE_ACKED] = True
|
||||
|
||||
self.set_remote_commitment(self.pending_commitment(REMOTE))
|
||||
self.set_remote_commitment()
|
||||
self.remote_commitment_to_be_revoked = prev_remote_commitment
|
||||
|
||||
return received_this_batch, sent_this_batch
|
||||
|
||||
def balance(self, subject):
|
||||
def balance(self, subject, ctn=None):
|
||||
"""
|
||||
This balance in mSAT is not including reserve and fees.
|
||||
So a node cannot actually use it's whole balance.
|
||||
|
@ -560,12 +494,15 @@ class Channel(PrintError):
|
|||
commited to later when the respective commitment
|
||||
transaction as been revoked.
|
||||
"""
|
||||
assert type(subject) is HTLCOwner
|
||||
initial = self.config[subject].initial_msat
|
||||
|
||||
initial -= sum(self.settled[subject])
|
||||
initial += sum(self.settled[-subject])
|
||||
for direction, htlc in self.hm.settled_htlcs(subject, ctn):
|
||||
if direction == SENT:
|
||||
initial -= htlc.amount_msat
|
||||
else:
|
||||
initial += htlc.amount_msat
|
||||
|
||||
assert initial == self.config[subject].amount_msat
|
||||
return initial
|
||||
|
||||
def balance_minus_outgoing_htlcs(self, subject):
|
||||
|
@ -573,48 +510,46 @@ class Channel(PrintError):
|
|||
This balance in mSAT, which includes the value of
|
||||
pending outgoing HTLCs, is used in the UI.
|
||||
"""
|
||||
return self.balance(subject)\
|
||||
- htlcsum(self.log[subject].adds.values())
|
||||
assert type(subject) is HTLCOwner
|
||||
ctn = self.hm.log[subject]['ctn'] + 1
|
||||
return self.balance(subject, ctn)\
|
||||
- htlcsum(self.hm.htlcs_by_direction(subject, SENT, ctn))
|
||||
|
||||
def available_to_spend(self, subject):
|
||||
"""
|
||||
This balance in mSAT, while technically correct, can
|
||||
not be used in the UI cause it fluctuates (commit fee)
|
||||
"""
|
||||
assert type(subject) is HTLCOwner
|
||||
return self.balance_minus_outgoing_htlcs(subject)\
|
||||
- htlcsum(self.log[subject].adds.values())\
|
||||
- self.config[-subject].reserve_sat * 1000\
|
||||
- calc_onchain_fees(
|
||||
# TODO should we include a potential new htlc, when we are called from receive_htlc?
|
||||
len(list(self.included_htlcs(subject, LOCAL)) + list(self.included_htlcs(subject, REMOTE))),
|
||||
len(self.included_htlcs(subject, SENT) + self.included_htlcs(subject, RECEIVED)),
|
||||
self.pending_feerate(subject),
|
||||
True, # for_us
|
||||
self.constraints.is_initiator,
|
||||
)[subject]
|
||||
|
||||
def amounts(self):
|
||||
remote_settled= htlcsum(self.htlcs(REMOTE, False))
|
||||
local_settled= htlcsum(self.htlcs(LOCAL, False))
|
||||
unsettled_local = htlcsum(self.htlcs(LOCAL, True))
|
||||
unsettled_remote = htlcsum(self.htlcs(REMOTE, True))
|
||||
remote_msat = self.config[REMOTE].amount_msat -\
|
||||
unsettled_remote + local_settled - remote_settled
|
||||
local_msat = self.config[LOCAL].amount_msat -\
|
||||
unsettled_local + remote_settled - local_settled
|
||||
return remote_msat, local_msat
|
||||
|
||||
def included_htlcs(self, subject, htlc_initiator, only_pending=True):
|
||||
def included_htlcs(self, subject, direction, ctn=None):
|
||||
"""
|
||||
return filter of non-dust htlcs for subjects commitment transaction, initiated by given party
|
||||
"""
|
||||
assert type(subject) is HTLCOwner
|
||||
assert type(direction) is Direction
|
||||
if ctn is None:
|
||||
ctn = self.config[subject].ctn
|
||||
feerate = self.pending_feerate(subject)
|
||||
conf = self.config[subject]
|
||||
weight = HTLC_SUCCESS_WEIGHT if subject != htlc_initiator else HTLC_TIMEOUT_WEIGHT
|
||||
htlcs = self.htlcs(htlc_initiator, only_pending=only_pending)
|
||||
if (subject, direction) in [(REMOTE, RECEIVED), (LOCAL, SENT)]:
|
||||
weight = HTLC_SUCCESS_WEIGHT
|
||||
else:
|
||||
weight = HTLC_TIMEOUT_WEIGHT
|
||||
htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn)
|
||||
fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000)
|
||||
return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs)
|
||||
return list(filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs))
|
||||
|
||||
def pending_feerate(self, subject):
|
||||
assert type(subject) is HTLCOwner
|
||||
candidate = self.constraints.feerate
|
||||
if self.pending_fee is not None:
|
||||
x = self.pending_fee.pending_feerate(subject)
|
||||
|
@ -623,81 +558,53 @@ class Channel(PrintError):
|
|||
return candidate
|
||||
|
||||
def pending_commitment(self, subject):
|
||||
assert type(subject) is HTLCOwner
|
||||
this_point = self.config[REMOTE].next_per_commitment_point if subject == REMOTE else self.points()[1]
|
||||
return self.make_commitment(subject, this_point)
|
||||
ctn = self.config[subject].ctn + 1
|
||||
feerate = self.pending_feerate(subject)
|
||||
return self.make_commitment(subject, this_point, ctn, feerate, True)
|
||||
|
||||
def current_commitment(self, subject):
|
||||
old_local_state = self.config[subject]
|
||||
self.config[subject]=self.config[subject]._replace(ctn=self.config[subject].ctn - 1)
|
||||
r = self.pending_commitment(subject)
|
||||
self.config[subject] = old_local_state
|
||||
return r
|
||||
assert type(subject) is HTLCOwner
|
||||
this_point = self.config[REMOTE].current_per_commitment_point if subject == REMOTE else self.points()[3]
|
||||
ctn = self.config[subject].ctn
|
||||
feerate = self.constraints.feerate
|
||||
return self.make_commitment(subject, this_point, ctn, feerate, False)
|
||||
|
||||
def total_msat(self, sub):
|
||||
return sum(self.settled[sub])
|
||||
|
||||
def htlcs(self, subject, only_pending):
|
||||
"""
|
||||
only_pending: require the htlc's settlement to be pending (needs additional signatures/acks)
|
||||
|
||||
sets returned with True and False are disjunct
|
||||
|
||||
only_pending true:
|
||||
skipped if settled or failed
|
||||
<=>
|
||||
included if not settled and not failed
|
||||
only_pending false:
|
||||
skipped if not (settled or failed)
|
||||
<=>
|
||||
included if not not (settled or failed)
|
||||
included if settled or failed
|
||||
"""
|
||||
update_log = self.log[subject]
|
||||
res = []
|
||||
for htlc in update_log.adds.values():
|
||||
locked_in = htlc.htlc_id in update_log.locked_in
|
||||
settled = htlc.htlc_id in update_log.settles
|
||||
failed = htlc.htlc_id in update_log.fails
|
||||
if not locked_in:
|
||||
continue
|
||||
if only_pending == (settled or failed):
|
||||
continue
|
||||
res.append(htlc)
|
||||
return res
|
||||
def total_msat(self, direction):
|
||||
assert type(direction) is Direction
|
||||
sub = LOCAL if direction == SENT else REMOTE
|
||||
return htlcsum(self.hm.settled_htlcs_by(sub, self.config[sub].ctn))
|
||||
|
||||
def settle_htlc(self, preimage, htlc_id):
|
||||
"""
|
||||
SettleHTLC attempts to settle an existing outstanding received HTLC.
|
||||
"""
|
||||
self.print_error("settle_htlc")
|
||||
log = self.log[REMOTE]
|
||||
htlc = log.adds[htlc_id]
|
||||
log = self.hm.log[REMOTE]
|
||||
htlc = log['adds'][htlc_id]
|
||||
assert htlc.payment_hash == sha256(preimage)
|
||||
assert htlc_id not in log.settles
|
||||
log.settles.add(htlc_id)
|
||||
assert htlc_id not in log['settles']
|
||||
self.hm.send_settle(htlc_id)
|
||||
# not saving preimage because it's already saved in LNWorker.invoices
|
||||
|
||||
def receive_htlc_settle(self, preimage, htlc_id):
|
||||
self.print_error("receive_htlc_settle")
|
||||
log = self.log[LOCAL]
|
||||
htlc = log.adds[htlc_id]
|
||||
log = self.hm.log[LOCAL]
|
||||
htlc = log['adds'][htlc_id]
|
||||
assert htlc.payment_hash == sha256(preimage)
|
||||
assert htlc_id not in log.settles
|
||||
assert htlc_id not in log['settles']
|
||||
self.hm.recv_settle(htlc_id)
|
||||
self.preimages[htlc_id] = preimage
|
||||
log.settles.add(htlc_id)
|
||||
# we don't save the preimage because we don't need to forward it anyway
|
||||
|
||||
def fail_htlc(self, htlc_id):
|
||||
self.print_error("fail_htlc")
|
||||
log = self.log[REMOTE]
|
||||
assert htlc_id not in log.fails
|
||||
log.fails.add(htlc_id)
|
||||
self.hm.send_fail(htlc_id)
|
||||
|
||||
def receive_fail_htlc(self, htlc_id):
|
||||
self.print_error("receive_fail_htlc")
|
||||
log = self.log[LOCAL]
|
||||
assert htlc_id not in log.fails
|
||||
log.fails.add(htlc_id)
|
||||
self.hm.recv_fail(htlc_id)
|
||||
|
||||
@property
|
||||
def current_height(self):
|
||||
|
@ -713,29 +620,7 @@ class Channel(PrintError):
|
|||
raise Exception("a fee update is already in progress")
|
||||
self.pending_fee = FeeUpdate(self, rate=feerate)
|
||||
|
||||
def remove_uncommitted_htlcs_from_log(self, subject):
|
||||
"""
|
||||
returns
|
||||
- the htlcs with uncommited (not locked in) htlcs removed
|
||||
- a list of htlc_ids that were removed
|
||||
"""
|
||||
removed = []
|
||||
htlcs = []
|
||||
log = self.log[subject]
|
||||
for i in log.adds.values():
|
||||
locked_in = i.htlc_id in log.locked_in
|
||||
if locked_in:
|
||||
htlcs.append(i._asdict())
|
||||
else:
|
||||
removed.append(i.htlc_id)
|
||||
return htlcs, removed
|
||||
|
||||
def to_save(self):
|
||||
# need to forget about uncommited htlcs
|
||||
# since we must assume they don't know about it,
|
||||
# if it was not acked
|
||||
remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE)
|
||||
local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL)
|
||||
to_save = {
|
||||
"local_config": self.config[LOCAL],
|
||||
"remote_config": self.config[REMOTE],
|
||||
|
@ -745,24 +630,10 @@ class Channel(PrintError):
|
|||
"funding_outpoint": self.funding_outpoint,
|
||||
"node_id": self.node_id,
|
||||
"remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked),
|
||||
"remote_log": remote_filtered,
|
||||
"local_log": local_filtered,
|
||||
"log": self.hm.to_save(),
|
||||
"onion_keys": str_bytes_dict_to_save(self.onion_keys),
|
||||
"settled_local": self.settled[LOCAL],
|
||||
"settled_remote": self.settled[REMOTE],
|
||||
"force_closed": self.get_state() == 'FORCE_CLOSING',
|
||||
}
|
||||
|
||||
# htlcs number must be monotonically increasing,
|
||||
# so we have to decrease the counter
|
||||
if len(remote_removed) != 0:
|
||||
assert min(remote_removed) < to_save['remote_config'].next_htlc_id
|
||||
to_save['remote_config'] = to_save['remote_config']._replace(next_htlc_id = min(remote_removed))
|
||||
|
||||
if len(local_removed) != 0:
|
||||
assert min(local_removed) < to_save['local_config'].next_htlc_id
|
||||
to_save['local_config'] = to_save['local_config']._replace(next_htlc_id = min(local_removed))
|
||||
|
||||
return to_save
|
||||
|
||||
def serialize(self):
|
||||
|
@ -792,17 +663,34 @@ class Channel(PrintError):
|
|||
def __str__(self):
|
||||
return str(self.serialize())
|
||||
|
||||
def make_commitment(self, subject, this_point) -> Transaction:
|
||||
remote_msat, local_msat = self.amounts()
|
||||
assert local_msat >= 0, local_msat
|
||||
assert remote_msat >= 0, remote_msat
|
||||
def make_commitment(self, subject, this_point, ctn, feerate, pending) -> Transaction:
|
||||
#if subject == REMOTE and not pending:
|
||||
# ctn -= 1
|
||||
assert type(subject) is HTLCOwner
|
||||
other = REMOTE if LOCAL == subject else LOCAL
|
||||
remote_msat, local_msat = self.balance(other, ctn), self.balance(subject, ctn)
|
||||
received_htlcs = self.hm.htlcs_by_direction(subject, SENT if subject == LOCAL else RECEIVED, ctn)
|
||||
sent_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED if subject == LOCAL else SENT, ctn)
|
||||
if subject != LOCAL:
|
||||
remote_msat -= htlcsum(received_htlcs)
|
||||
local_msat -= htlcsum(sent_htlcs)
|
||||
else:
|
||||
remote_msat -= htlcsum(sent_htlcs)
|
||||
local_msat -= htlcsum(received_htlcs)
|
||||
assert remote_msat >= 0
|
||||
assert local_msat >= 0
|
||||
# same htlcs as before, but now without dust.
|
||||
received_htlcs = self.included_htlcs(subject, SENT if subject == LOCAL else RECEIVED, ctn)
|
||||
sent_htlcs = self.included_htlcs(subject, RECEIVED if subject == LOCAL else SENT, ctn)
|
||||
|
||||
this_config = self.config[subject]
|
||||
other_config = self.config[-subject]
|
||||
other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)
|
||||
this_htlc_pubkey = derive_pubkey(this_config.htlc_basepoint.pubkey, this_point)
|
||||
other_revocation_pubkey = derive_blinded_pubkey(other_config.revocation_basepoint.pubkey, this_point)
|
||||
htlcs = [] # type: List[ScriptHtlc]
|
||||
def append_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool):
|
||||
for is_received_htlc, htlc_list in zip((subject != LOCAL, subject == LOCAL), (received_htlcs, sent_htlcs)):
|
||||
for htlc in htlc_list:
|
||||
htlcs.append(ScriptHtlc(make_htlc_output_witness_script(
|
||||
is_received_htlc=is_received_htlc,
|
||||
remote_revocation_pubkey=other_revocation_pubkey,
|
||||
|
@ -810,15 +698,14 @@ class Channel(PrintError):
|
|||
local_htlc_pubkey=this_htlc_pubkey,
|
||||
payment_hash=htlc.payment_hash,
|
||||
cltv_expiry=htlc.cltv_expiry), htlc))
|
||||
for htlc in self.included_htlcs(subject, -subject):
|
||||
append_htlc(htlc, is_received_htlc=True)
|
||||
for htlc in self.included_htlcs(subject, subject):
|
||||
append_htlc(htlc, is_received_htlc=False)
|
||||
if subject != LOCAL:
|
||||
remote_msat, local_msat = local_msat, remote_msat
|
||||
onchain_fees = calc_onchain_fees(
|
||||
len(htlcs),
|
||||
feerate,
|
||||
self.constraints.is_initiator == (subject == LOCAL),
|
||||
)
|
||||
payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
|
||||
return make_commitment(
|
||||
self.config[subject].ctn + 1,
|
||||
ctn,
|
||||
this_config.multisig_key.pubkey,
|
||||
other_config.multisig_key.pubkey,
|
||||
payment_pubkey,
|
||||
|
@ -832,12 +719,7 @@ class Channel(PrintError):
|
|||
local_msat,
|
||||
remote_msat,
|
||||
this_config.dust_limit_sat,
|
||||
calc_onchain_fees(
|
||||
len(htlcs),
|
||||
self.pending_feerate(subject),
|
||||
subject == LOCAL,
|
||||
self.constraints.is_initiator,
|
||||
),
|
||||
onchain_fees,
|
||||
htlcs=htlcs)
|
||||
|
||||
def get_local_index(self):
|
||||
|
@ -850,8 +732,8 @@ class Channel(PrintError):
|
|||
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
|
||||
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
|
||||
},
|
||||
self.config[LOCAL].amount_msat,
|
||||
self.config[REMOTE].amount_msat,
|
||||
self.balance(LOCAL),
|
||||
self.balance(REMOTE),
|
||||
(TYPE_SCRIPT, bh2u(local_script)),
|
||||
(TYPE_SCRIPT, bh2u(remote_script)),
|
||||
[], self.config[LOCAL].dust_limit_sat)
|
||||
|
@ -867,22 +749,21 @@ class Channel(PrintError):
|
|||
sig = ecc.sig_string_from_der_sig(der_sig[:-1])
|
||||
return sig, closing_tx
|
||||
|
||||
def assert_signature_fits(self, tx):
|
||||
def signature_fits(self, tx):
|
||||
remote_sig = self.config[LOCAL].current_commitment_signature
|
||||
if remote_sig: # only None in test
|
||||
preimage_hex = tx.serialize_preimage(0)
|
||||
pre_hash = sha256d(bfh(preimage_hex))
|
||||
if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash):
|
||||
self.print_error("WARNING: commitment signature inconsistency, cannot force close")
|
||||
assert remote_sig
|
||||
res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash)
|
||||
return res
|
||||
|
||||
def force_close_tx(self):
|
||||
tx = self.local_commitment
|
||||
assert self.signature_fits(tx)
|
||||
tx = Transaction(str(tx))
|
||||
tx.deserialize(True)
|
||||
self.assert_signature_fits(tx)
|
||||
tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)})
|
||||
remote_sig = self.config[LOCAL].current_commitment_signature
|
||||
if remote_sig: # only None in test
|
||||
remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01"
|
||||
sigs = tx._inputs[0]["signatures"]
|
||||
none_idx = sigs.index(None)
|
||||
|
@ -894,11 +775,13 @@ class Channel(PrintError):
|
|||
""" A map from commitment number to list of HTLCs in
|
||||
their latest two commitment transactions.
|
||||
The oldest might have been revoked. """
|
||||
old_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator, only_pending=False))
|
||||
assert type(htlc_initiator) is HTLCOwner
|
||||
direction = RECEIVED if htlc_initiator == LOCAL else SENT
|
||||
old_ctn = self.config[REMOTE].ctn
|
||||
old_htlcs = self.included_htlcs(REMOTE, direction, ctn=old_ctn)
|
||||
|
||||
old_logs = dict(self.lock_in_htlc_changes(LOCAL))
|
||||
new_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator))
|
||||
self.log = old_logs
|
||||
new_ctn = self.config[REMOTE].ctn+1
|
||||
new_htlcs = self.included_htlcs(REMOTE, direction, ctn=new_ctn)
|
||||
|
||||
return {self.config[REMOTE].ctn: old_htlcs,
|
||||
self.config[REMOTE].ctn+1: new_htlcs, }
|
||||
return {old_ctn: old_htlcs,
|
||||
new_ctn: new_htlcs, }
|
||||
|
|
159
electrum/lnhtlc.py
Normal file
159
electrum/lnhtlc.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
from copy import deepcopy
|
||||
from .lnutil import SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, UpdateAddHtlc, Direction
|
||||
from .util import bh2u
|
||||
|
||||
class HTLCManager:
|
||||
def __init__(self, log=None):
|
||||
self.expect_sig = {SENT: False, RECEIVED: False}
|
||||
if log is None:
|
||||
initial = {'ctn': 0, 'adds': {}, 'locked_in': {}, 'settles': {}, 'fails': {}}
|
||||
log = {LOCAL: deepcopy(initial), REMOTE: deepcopy(initial)}
|
||||
else:
|
||||
assert type(log) is dict
|
||||
log = {HTLCOwner(int(x)): y for x, y in deepcopy(log).items()}
|
||||
for sub in (LOCAL, REMOTE):
|
||||
log[sub]['adds'] = {int(x): UpdateAddHtlc(*y) for x, y in log[sub]['adds'].items()}
|
||||
coerceHtlcOwner2IntMap = lambda x: {HTLCOwner(int(y)): z for y, z in x.items()}
|
||||
log[sub]['locked_in'] = {int(x): coerceHtlcOwner2IntMap(y) for x, y in log[sub]['locked_in'].items()}
|
||||
log[sub]['settles'] = {int(x): y for x, y in log[sub]['settles'].items()}
|
||||
log[sub]['fails'] = {int(x): y for x, y in log[sub]['fails'].items()}
|
||||
self.log = log
|
||||
|
||||
def to_save(self):
|
||||
x = deepcopy(self.log)
|
||||
for sub in (LOCAL, REMOTE):
|
||||
d = {}
|
||||
for htlc_id, htlc in x[sub]['adds'].items():
|
||||
d[htlc_id] = (htlc[0], bh2u(htlc[1])) + htlc[2:]
|
||||
x[sub]['adds'] = d
|
||||
return x
|
||||
|
||||
def send_htlc(self, htlc):
|
||||
htlc_id = htlc.htlc_id
|
||||
adds = self.log[LOCAL]['adds']
|
||||
assert type(adds) is not str
|
||||
adds[htlc_id] = htlc
|
||||
self.log[LOCAL]['locked_in'][htlc_id] = {LOCAL: None, REMOTE: self.log[REMOTE]['ctn']+1}
|
||||
self.expect_sig[SENT] = True
|
||||
return htlc
|
||||
|
||||
def recv_htlc(self, htlc):
|
||||
htlc_id = htlc.htlc_id
|
||||
self.log[REMOTE]['htlc_id'] = htlc_id
|
||||
self.log[REMOTE]['adds'][htlc_id] = htlc
|
||||
l = self.log[REMOTE]['locked_in'][htlc_id] = {LOCAL: self.log[LOCAL]['ctn']+1, REMOTE: None}
|
||||
self.expect_sig[RECEIVED] = True
|
||||
|
||||
def send_ctx(self):
|
||||
next_ctn = self.log[REMOTE]['ctn'] + 1
|
||||
|
||||
for locked_in in self.log[REMOTE]['locked_in'].values():
|
||||
if locked_in[REMOTE] is None:
|
||||
locked_in[REMOTE] = next_ctn
|
||||
|
||||
self.expect_sig[SENT] = False
|
||||
|
||||
#return Sig(self.pending_htlcs(REMOTE), next_ctn)
|
||||
|
||||
def recv_ctx(self):
|
||||
next_ctn = self.log[LOCAL]['ctn'] + 1
|
||||
|
||||
for locked_in in self.log[LOCAL]['locked_in'].values():
|
||||
if locked_in[LOCAL] is None:
|
||||
locked_in[LOCAL] = next_ctn
|
||||
|
||||
self.expect_sig[SENT] = False
|
||||
|
||||
def send_rev(self):
|
||||
self.log[LOCAL]['ctn'] += 1
|
||||
|
||||
def recv_rev(self):
|
||||
self.log[REMOTE]['ctn'] += 1
|
||||
did_set_htlc_height = False
|
||||
for htlc_id, ctnheights in self.log[LOCAL]['locked_in'].items():
|
||||
if ctnheights[LOCAL] is None:
|
||||
did_set_htlc_height = True
|
||||
assert ctnheights[REMOTE] == self.log[REMOTE]['ctn']
|
||||
ctnheights[LOCAL] = ctnheights[REMOTE]
|
||||
return did_set_htlc_height
|
||||
|
||||
def htlcs_by_direction(self, subject, direction, ctn=None):
|
||||
"""
|
||||
direction is relative to subject!
|
||||
"""
|
||||
assert type(subject) is HTLCOwner
|
||||
assert type(direction) is Direction
|
||||
if ctn is None:
|
||||
ctn = self.log[subject]['ctn']
|
||||
l = []
|
||||
if direction == SENT and subject == LOCAL:
|
||||
party = LOCAL
|
||||
elif direction == RECEIVED and subject == REMOTE:
|
||||
party = LOCAL
|
||||
else:
|
||||
party = REMOTE
|
||||
for htlc_id, ctnheights in self.log[party]['locked_in'].items():
|
||||
htlc_height = ctnheights[subject]
|
||||
if htlc_height is None:
|
||||
include = not self.expect_sig[RECEIVED if party == LOCAL else SENT] and ctnheights[-subject] <= ctn
|
||||
else:
|
||||
include = htlc_height <= ctn
|
||||
if include:
|
||||
settles = self.log[party]['settles']
|
||||
if htlc_id not in settles or settles[htlc_id] > ctn:
|
||||
fails = self.log[party]['fails']
|
||||
if htlc_id not in fails or fails[htlc_id] > ctn:
|
||||
l.append(self.log[party]['adds'][htlc_id])
|
||||
return l
|
||||
|
||||
def htlcs(self, subject, ctn=None):
|
||||
assert type(subject) is HTLCOwner
|
||||
if ctn is None:
|
||||
ctn = self.log[subject]['ctn']
|
||||
l = []
|
||||
l += [(SENT, x) for x in self.htlcs_by_direction(subject, SENT, ctn)]
|
||||
l += [(RECEIVED, x) for x in self.htlcs_by_direction(subject, RECEIVED, ctn)]
|
||||
return l
|
||||
|
||||
def current_htlcs(self, subject):
|
||||
assert type(subject) is HTLCOwner
|
||||
ctn = self.log[subject]['ctn']
|
||||
return self.htlcs(subject, ctn)
|
||||
|
||||
def pending_htlcs(self, subject):
|
||||
assert type(subject) is HTLCOwner
|
||||
ctn = self.log[subject]['ctn'] + 1
|
||||
return self.htlcs(subject, ctn)
|
||||
|
||||
def send_settle(self, htlc_id):
|
||||
self.log[REMOTE]['settles'][htlc_id] = self.log[REMOTE]['ctn'] + 1
|
||||
|
||||
def recv_settle(self, htlc_id):
|
||||
self.log[LOCAL]['settles'][htlc_id] = self.log[LOCAL]['ctn'] + 1
|
||||
|
||||
def settled_htlcs_by(self, subject, ctn=None):
|
||||
assert type(subject) is HTLCOwner
|
||||
if ctn is None:
|
||||
ctn = self.log[subject]['ctn']
|
||||
return [self.log[subject]['adds'][htlc_id] for htlc_id, height in self.log[subject]['settles'].items() if height <= ctn]
|
||||
|
||||
def settled_htlcs(self, subject, ctn=None):
|
||||
assert type(subject) is HTLCOwner
|
||||
if ctn is None:
|
||||
ctn = self.log[subject]['ctn']
|
||||
sent = [(SENT, x) for x in self.settled_htlcs_by(subject, ctn)]
|
||||
other = subject.inverted()
|
||||
received = [(RECEIVED, x) for x in self.settled_htlcs_by(other, ctn)]
|
||||
return sent + received
|
||||
|
||||
def received_in_ctn(self, ctn):
|
||||
return [self.log[REMOTE]['adds'][htlc_id] for htlc_id, height in self.log[REMOTE]['settles'].items() if height == ctn]
|
||||
|
||||
def sent_in_ctn(self, ctn):
|
||||
return [self.log[LOCAL]['adds'][htlc_id] for htlc_id, height in self.log[LOCAL]['settles'].items() if height == ctn]
|
||||
|
||||
def send_fail(self, htlc_id):
|
||||
self.log[REMOTE]['fails'][htlc_id] = self.log[REMOTE]['ctn'] + 1
|
||||
|
||||
def recv_fail(self, htlc_id):
|
||||
self.log[LOCAL]['fails'][htlc_id] = self.log[LOCAL]['ctn'] + 1
|
|
@ -9,15 +9,15 @@ from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
|
|||
from . import ecc
|
||||
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
|
||||
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
|
||||
make_htlc_tx_witness, make_htlc_tx_with_open_channel,
|
||||
make_htlc_tx_witness, make_htlc_tx_with_open_channel, UpdateAddHtlc,
|
||||
LOCAL, REMOTE, make_htlc_output_witness_script, UnknownPaymentHash,
|
||||
get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
|
||||
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret)
|
||||
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED)
|
||||
from .transaction import Transaction, TxOutput, construct_witness
|
||||
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .lnchan import Channel, UpdateAddHtlc
|
||||
from .lnchan import Channel
|
||||
|
||||
|
||||
def maybe_create_sweeptx_for_their_ctx_to_remote(ctx: Transaction, sweep_address: str,
|
||||
|
@ -106,7 +106,7 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
|
|||
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
|
||||
assert ctn == chan.config[REMOTE].ctn
|
||||
# received HTLCs, in their ctx
|
||||
received_htlcs = chan.included_htlcs(REMOTE, LOCAL, False)
|
||||
received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn)
|
||||
for htlc in received_htlcs:
|
||||
direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True)
|
||||
if direct_sweep_tx:
|
||||
|
@ -114,7 +114,7 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
|
|||
if secondstage_sweep_tx:
|
||||
txs[htlc_tx.txid()] = secondstage_sweep_tx
|
||||
# offered HTLCs, in their ctx
|
||||
offered_htlcs = chan.included_htlcs(REMOTE, REMOTE, False)
|
||||
offered_htlcs = chan.included_htlcs(REMOTE, SENT, ctn)
|
||||
for htlc in offered_htlcs:
|
||||
direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False)
|
||||
if direct_sweep_tx:
|
||||
|
@ -181,16 +181,14 @@ def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction,
|
|||
is_revocation=False)
|
||||
return htlc_tx, to_wallet_tx
|
||||
# offered HTLCs, in our ctx --> "timeout"
|
||||
# TODO consider carefully if "included_htlcs" is what we need here
|
||||
offered_htlcs = list(chan.included_htlcs(LOCAL, LOCAL)) # type: List[UpdateAddHtlc]
|
||||
# received HTLCs, in our ctx --> "success"
|
||||
offered_htlcs = chan.included_htlcs(LOCAL, SENT, ctn) # type: List[UpdateAddHtlc]
|
||||
received_htlcs = chan.included_htlcs(LOCAL, RECEIVED, ctn) # type: List[UpdateAddHtlc]
|
||||
for htlc in offered_htlcs:
|
||||
htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=False)
|
||||
if htlc_tx and to_wallet_tx:
|
||||
txs[to_wallet_tx.prevout(0)] = to_wallet_tx
|
||||
txs[htlc_tx.prevout(0)] = htlc_tx
|
||||
# received HTLCs, in our ctx --> "success"
|
||||
# TODO consider carefully if "included_htlcs" is what we need here
|
||||
received_htlcs = list(chan.included_htlcs(LOCAL, REMOTE)) # type: List[UpdateAddHtlc]
|
||||
for htlc in received_htlcs:
|
||||
htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=True)
|
||||
if htlc_tx and to_wallet_tx:
|
||||
|
@ -332,7 +330,7 @@ def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes,
|
|||
htlc=htlc,
|
||||
name=f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}',
|
||||
cltv_expiry=0 if is_received_htlc else htlc.cltv_expiry)
|
||||
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc, we_receive=is_received_htlc)
|
||||
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc, we_receive=is_received_htlc, ctx=ctx)
|
||||
local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
|
||||
txin = htlc_tx.inputs()[0]
|
||||
witness_program = bfh(Transaction.get_preimage_script(txin))
|
||||
|
|
|
@ -21,7 +21,7 @@ from .lnaddr import lndecode
|
|||
from .keystore import BIP32_KeyStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .lnchan import Channel, UpdateAddHtlc
|
||||
from .lnchan import Channel
|
||||
|
||||
|
||||
HTLC_TIMEOUT_WEIGHT = 663
|
||||
|
@ -35,7 +35,6 @@ OnlyPubkeyKeypair = namedtuple("OnlyPubkeyKeypair", ["pubkey"])
|
|||
class LocalConfig(NamedTuple):
|
||||
# shared channel config fields (DUPLICATED code!!)
|
||||
ctn: int
|
||||
amount_msat: int
|
||||
next_htlc_id: int
|
||||
payment_basepoint: 'Keypair'
|
||||
multisig_key: 'Keypair'
|
||||
|
@ -54,12 +53,12 @@ class LocalConfig(NamedTuple):
|
|||
was_announced: bool
|
||||
current_commitment_signature: Optional[bytes]
|
||||
current_htlc_signatures: List[bytes]
|
||||
got_sig_for_next: bool
|
||||
|
||||
|
||||
class RemoteConfig(NamedTuple):
|
||||
# shared channel config fields (DUPLICATED code!!)
|
||||
ctn: int
|
||||
amount_msat: int
|
||||
next_htlc_id: int
|
||||
payment_basepoint: 'Keypair'
|
||||
multisig_key: 'Keypair'
|
||||
|
@ -364,7 +363,7 @@ def make_htlc_tx_with_open_channel(chan: 'Channel', pcp: bytes, for_us: bool,
|
|||
# FIXME handle htlc_address collision
|
||||
# also: https://github.com/lightningnetwork/lightning-rfc/issues/448
|
||||
prevout_idx = commit.get_output_idx_from_address(htlc_address)
|
||||
assert prevout_idx is not None
|
||||
assert prevout_idx is not None, (htlc_address, commit.outputs(), extract_ctn_from_tx_and_chan(commit, chan))
|
||||
htlc_tx_inputs = make_htlc_tx_inputs(
|
||||
commit.txid(), prevout_idx,
|
||||
amount_msat=amount_msat,
|
||||
|
@ -395,11 +394,16 @@ class HTLCOwner(IntFlag):
|
|||
LOCAL = 1
|
||||
REMOTE = -LOCAL
|
||||
|
||||
SENT = LOCAL
|
||||
RECEIVED = REMOTE
|
||||
def inverted(self):
|
||||
return HTLCOwner(-self)
|
||||
|
||||
class Direction(IntFlag):
|
||||
SENT = 3
|
||||
RECEIVED = 4
|
||||
|
||||
SENT = Direction.SENT
|
||||
RECEIVED = Direction.RECEIVED
|
||||
|
||||
SENT = HTLCOwner.SENT
|
||||
RECEIVED = HTLCOwner.RECEIVED
|
||||
LOCAL = HTLCOwner.LOCAL
|
||||
REMOTE = HTLCOwner.REMOTE
|
||||
|
||||
|
@ -420,8 +424,7 @@ def make_commitment_outputs(fees_per_participant: Mapping[HTLCOwner, int], local
|
|||
c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))
|
||||
return htlc_outputs, c_outputs_filtered
|
||||
|
||||
def calc_onchain_fees(num_htlcs, feerate, for_us, we_are_initiator):
|
||||
we_pay_fee = for_us == we_are_initiator
|
||||
def calc_onchain_fees(num_htlcs, feerate, we_pay_fee):
|
||||
overall_weight = 500 + 172 * num_htlcs + 224
|
||||
fee = feerate * overall_weight
|
||||
fee = fee // 1000 * 1000
|
||||
|
@ -451,7 +454,7 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
|
|||
htlc_outputs, c_outputs_filtered = make_commitment_outputs(fees_per_participant, local_amount, remote_amount,
|
||||
(bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat)
|
||||
|
||||
assert sum(x.value for x in c_outputs_filtered) <= funding_sat
|
||||
assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
|
||||
|
||||
# create commitment tx
|
||||
tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
|
||||
|
@ -649,3 +652,20 @@ def format_short_channel_id(short_channel_id: Optional[bytes]):
|
|||
return str(int.from_bytes(short_channel_id[:3], 'big')) \
|
||||
+ 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \
|
||||
+ 'x' + str(int.from_bytes(short_channel_id[6:], 'big'))
|
||||
|
||||
class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'htlc_id'])):
|
||||
"""
|
||||
This whole class body is so that if you pass a hex-string as payment_hash,
|
||||
it is decoded to bytes. Bytes can't be saved to disk, so we save hex-strings.
|
||||
"""
|
||||
__slots__ = ()
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
if type(args[1]) is str:
|
||||
args[1] = bfh(args[1])
|
||||
return super().__new__(cls, *args)
|
||||
if type(kwargs['payment_hash']) is str:
|
||||
kwargs['payment_hash'] = bfh(kwargs['payment_hash'])
|
||||
return super().__new__(cls, **kwargs)
|
||||
|
||||
|
|
|
@ -29,13 +29,14 @@ from .lntransport import LNResponderTransport
|
|||
from .lnbase import Peer
|
||||
from .lnaddr import lnencode, LnAddr, lndecode
|
||||
from .ecc import der_sig_from_sig_string
|
||||
from .lnchan import Channel, ChannelJsonEncoder, UpdateAddHtlc
|
||||
from .lnchan import Channel, ChannelJsonEncoder
|
||||
from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
|
||||
get_compressed_pubkey_from_bech32, extract_nodeid,
|
||||
PaymentFailure, split_host_port, ConnStringFormatError,
|
||||
generate_keypair, LnKeyFamily, LOCAL, REMOTE,
|
||||
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
|
||||
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner)
|
||||
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
|
||||
UpdateAddHtlc, Direction)
|
||||
from .i18n import _
|
||||
from .lnrouter import RouteEdge, is_route_sane_to_use
|
||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
|
@ -66,7 +67,7 @@ class LNWorker(PrintError):
|
|||
def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'):
|
||||
self.wallet = wallet
|
||||
# invoices we are currently trying to pay (might be pending HTLCs on a commitment transaction)
|
||||
self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int], bytes]]
|
||||
self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int], str]]
|
||||
self.sweep_address = wallet.get_receiving_address()
|
||||
self.network = network
|
||||
self.channel_db = self.network.channel_db
|
||||
|
@ -75,12 +76,15 @@ class LNWorker(PrintError):
|
|||
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
|
||||
self.config = network.config
|
||||
self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer
|
||||
self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice)
|
||||
self.channels = {} # type: Dict[bytes, Channel]
|
||||
for x in wallet.storage.get("channels", []):
|
||||
c = Channel(x, sweep_address=self.sweep_address, payment_completed=self.payment_completed)
|
||||
self.channels[c.channel_id] = c
|
||||
c.lnwatcher = network.lnwatcher
|
||||
self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice)
|
||||
c.get_preimage_and_invoice = self.get_invoice
|
||||
self.channels[c.channel_id] = c
|
||||
c.set_remote_commitment()
|
||||
c.set_local_commitment(c.current_commitment(LOCAL))
|
||||
for chan_id, chan in self.channels.items():
|
||||
self.network.lnwatcher.watch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str())
|
||||
self._last_tried_peer = {} # LNPeerAddr -> unix timestamp
|
||||
|
@ -116,6 +120,7 @@ class LNWorker(PrintError):
|
|||
self.print_error('saved lightning gossip timestamp')
|
||||
|
||||
def payment_completed(self, chan, direction, htlc, preimage):
|
||||
assert type(direction) is Direction
|
||||
chan_id = chan.channel_id
|
||||
if direction == SENT:
|
||||
assert htlc.payment_hash not in self.invoices
|
||||
|
@ -166,6 +171,7 @@ class LNWorker(PrintError):
|
|||
unsettled = []
|
||||
inflight = []
|
||||
for date, direction, htlc, hex_preimage, hex_chan_id in completed:
|
||||
direction = Direction(direction)
|
||||
if chan_id is not None:
|
||||
if bfh(hex_chan_id) != chan_id:
|
||||
continue
|
||||
|
@ -175,12 +181,12 @@ class LNWorker(PrintError):
|
|||
else:
|
||||
preimage = bfh(hex_preimage)
|
||||
# FIXME use fromisoformat when minimum Python is 3.7
|
||||
settled.append((datetime.fromtimestamp(date, timezone.utc), HTLCOwner(direction), htlcobj, preimage))
|
||||
settled.append((datetime.fromtimestamp(date, timezone.utc), direction, htlcobj, preimage))
|
||||
for preimage, pay_req in invoices.values():
|
||||
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
||||
unsettled.append((addr, bfh(preimage), pay_req))
|
||||
for pay_req, amount_sat, this_chan_id in self.paying.values():
|
||||
if chan_id is not None and this_chan_id != chan_id:
|
||||
if chan_id is not None and bfh(this_chan_id) != chan_id:
|
||||
continue
|
||||
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
||||
if amount_sat is not None:
|
||||
|
@ -194,7 +200,7 @@ class LNWorker(PrintError):
|
|||
def find_htlc_for_addr(self, addr, whitelist=None):
|
||||
channels = [y for x,y in self.channels.items() if x in whitelist or whitelist is None]
|
||||
for chan in channels:
|
||||
for htlc in chan.log[LOCAL].adds.values():
|
||||
for htlc in chan.hm.log[LOCAL]['adds'].values():
|
||||
if htlc.payment_hash == addr.paymenthash:
|
||||
return htlc
|
||||
|
||||
|
@ -319,7 +325,7 @@ class LNWorker(PrintError):
|
|||
self.print_error('they force closed', funding_outpoint)
|
||||
encumbered_sweeptxs = chan.remote_sweeptxs
|
||||
else:
|
||||
self.print_error('not sure who closed', funding_outpoint)
|
||||
self.print_error('not sure who closed', funding_outpoint, txid)
|
||||
return
|
||||
# sweep
|
||||
for prevout, spender in spenders.items():
|
||||
|
@ -456,7 +462,7 @@ class LNWorker(PrintError):
|
|||
break
|
||||
else:
|
||||
assert False, 'Found route with short channel ID we don\'t have: ' + repr(route[0].short_channel_id)
|
||||
self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat, chan_id)
|
||||
self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat, bh2u(chan_id))
|
||||
self.wallet.storage.put('lightning_payments_inflight', self.paying)
|
||||
self.wallet.storage.write()
|
||||
return addr, peer, self._pay_to_route(route, addr)
|
||||
|
@ -623,8 +629,8 @@ class LNWorker(PrintError):
|
|||
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
|
||||
for channel_id, chan in self.channels.items():
|
||||
yield {
|
||||
'local_htlcs': json.loads(encoder.encode(chan.log[LOCAL ]._asdict())),
|
||||
'remote_htlcs': json.loads(encoder.encode(chan.log[REMOTE]._asdict())),
|
||||
'local_htlcs': json.loads(encoder.encode(chan.hm.log[LOCAL ])),
|
||||
'remote_htlcs': json.loads(encoder.encode(chan.hm.log[REMOTE])),
|
||||
'channel_id': bh2u(chan.short_channel_id),
|
||||
'channel_point': chan.funding_outpoint.to_str(),
|
||||
'state': chan.get_state(),
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
import unittest
|
||||
import os
|
||||
import binascii
|
||||
from pprint import pformat
|
||||
|
||||
from electrum import bitcoin
|
||||
from electrum import lnbase
|
||||
|
@ -30,6 +31,7 @@ from electrum import lnutil
|
|||
from electrum import bip32 as bip32_utils
|
||||
from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
|
||||
from electrum.ecc import sig_string_from_der_sig
|
||||
from electrum.util import set_verbosity
|
||||
|
||||
one_bitcoin_in_msat = bitcoin.COIN * 1000
|
||||
|
||||
|
@ -54,9 +56,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
|
|||
max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5,
|
||||
max_accepted_htlcs=5,
|
||||
initial_msat=remote_amount,
|
||||
ctn = 0,
|
||||
ctn = -1,
|
||||
next_htlc_id = 0,
|
||||
amount_msat=remote_amount,
|
||||
reserve_sat=0,
|
||||
|
||||
next_per_commitment_point=nex,
|
||||
|
@ -76,7 +77,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
|
|||
initial_msat=local_amount,
|
||||
ctn = 0,
|
||||
next_htlc_id = 0,
|
||||
amount_msat=local_amount,
|
||||
reserve_sat=0,
|
||||
|
||||
per_commitment_secret_seed=seed,
|
||||
|
@ -84,6 +84,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
|
|||
was_announced=False,
|
||||
current_commitment_signature=None,
|
||||
current_htlc_signatures=None,
|
||||
got_sig_for_next=False,
|
||||
),
|
||||
"constraints":lnbase.ChannelConstraints(
|
||||
capacity=funding_sat,
|
||||
|
@ -105,7 +106,7 @@ def bip32(sequence):
|
|||
return k
|
||||
|
||||
def create_test_channels(feerate=6000, local=None, remote=None):
|
||||
funding_txid = binascii.hexlify(os.urandom(32)).decode("ascii")
|
||||
funding_txid = binascii.hexlify(b"\x01"*32).decode("ascii")
|
||||
funding_index = 0
|
||||
funding_sat = ((local + remote) // 1000) if local is not None and remote is not None else (bitcoin.COIN * 10)
|
||||
local_amount = local if local is not None else (funding_sat * 1000 // 2)
|
||||
|
@ -117,23 +118,52 @@ def create_test_channels(feerate=6000, local=None, remote=None):
|
|||
alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys]
|
||||
bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys]
|
||||
|
||||
alice_seed = os.urandom(32)
|
||||
bob_seed = os.urandom(32)
|
||||
alice_seed = b"\x01" * 32
|
||||
bob_seed = b"\x02" * 32
|
||||
|
||||
alice_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big"))
|
||||
alice_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||
bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big"))
|
||||
bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||
alice_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big"))
|
||||
bob_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big"))
|
||||
|
||||
alice, bob = \
|
||||
lnchan.Channel(
|
||||
create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), name="alice"), \
|
||||
create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, None, bob_first, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), name="alice"), \
|
||||
lnchan.Channel(
|
||||
create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), name="bob")
|
||||
create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, None, alice_first, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), name="bob")
|
||||
|
||||
alice.set_state('OPEN')
|
||||
bob.set_state('OPEN')
|
||||
|
||||
a_out = alice.current_commitment(LOCAL).outputs()
|
||||
b_out = bob.pending_commitment(REMOTE).outputs()
|
||||
assert a_out == b_out, "\n" + pformat((a_out, b_out))
|
||||
|
||||
sig_from_bob, a_htlc_sigs = bob.sign_next_commitment()
|
||||
sig_from_alice, b_htlc_sigs = alice.sign_next_commitment()
|
||||
|
||||
assert len(a_htlc_sigs) == 0
|
||||
assert len(b_htlc_sigs) == 0
|
||||
|
||||
alice.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob)
|
||||
bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice)
|
||||
|
||||
alice.set_local_commitment(alice.current_commitment(LOCAL))
|
||||
bob.set_local_commitment(bob.current_commitment(LOCAL))
|
||||
|
||||
alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||
bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||
|
||||
alice.config[REMOTE] = alice.config[REMOTE]._replace(next_per_commitment_point=bob_second, current_per_commitment_point=bob_first)
|
||||
bob.config[REMOTE] = bob.config[REMOTE]._replace(next_per_commitment_point=alice_second, current_per_commitment_point=alice_first)
|
||||
|
||||
alice.set_remote_commitment()
|
||||
bob.set_remote_commitment()
|
||||
|
||||
alice.remote_commitment_to_be_revoked = alice.remote_commitment
|
||||
bob.remote_commitment_to_be_revoked = bob.remote_commitment
|
||||
|
||||
alice.config[REMOTE] = alice.config[REMOTE]._replace(ctn=0)
|
||||
bob.config[REMOTE] = bob.config[REMOTE]._replace(ctn=0)
|
||||
|
||||
return alice, bob
|
||||
|
||||
class TestFee(unittest.TestCase):
|
||||
|
@ -141,11 +171,13 @@ class TestFee(unittest.TestCase):
|
|||
test
|
||||
https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2
|
||||
"""
|
||||
def test_SimpleAddSettleWorkflow(self):
|
||||
def test_fee(self):
|
||||
alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000)
|
||||
self.assertIn(9999817, [x[2] for x in alice_channel.local_commitment.outputs()])
|
||||
|
||||
class TestChannel(unittest.TestCase):
|
||||
maxDiff = 999
|
||||
|
||||
def assertOutputExistsByValue(self, tx, amt_sat):
|
||||
for typ, scr, val in tx.outputs():
|
||||
if val == amt_sat:
|
||||
|
@ -153,6 +185,10 @@ class TestChannel(unittest.TestCase):
|
|||
else:
|
||||
self.assertFalse()
|
||||
|
||||
@staticmethod
|
||||
def setUpClass():
|
||||
set_verbosity(True)
|
||||
|
||||
def setUp(self):
|
||||
# Create a test channel which will be used for the duration of this
|
||||
# unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||
|
@ -171,12 +207,15 @@ class TestChannel(unittest.TestCase):
|
|||
# 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)
|
||||
self.assertNotEqual(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1), set())
|
||||
|
||||
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)
|
||||
|
||||
self.assertEqual(1, self.bob_channel.hm.log[LOCAL]['ctn'] + 1)
|
||||
self.assertNotEqual(self.bob_channel.hm.htlcs_by_direction(LOCAL, RECEIVED, 1), set())
|
||||
after = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE)
|
||||
afterLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL)
|
||||
|
||||
|
@ -185,7 +224,7 @@ class TestChannel(unittest.TestCase):
|
|||
|
||||
self.bob_pending_remote_balance = after
|
||||
|
||||
self.htlc = self.bob_channel.log[lnutil.REMOTE].adds[0]
|
||||
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')
|
||||
|
@ -193,32 +232,65 @@ class TestChannel(unittest.TestCase):
|
|||
bob_idx = self.bob_channel.add_htlc(self.htlc_dict)
|
||||
alice_idx = self.alice_channel.receive_htlc(self.htlc_dict)
|
||||
self.alice_channel.receive_new_commitment(*self.bob_channel.sign_next_commitment())
|
||||
self.assertEqual(len(self.alice_channel.pending_commitment(REMOTE).outputs()), 3)
|
||||
self.assertEqual(len(self.alice_channel.pending_commitment(REMOTE).outputs()), 4)
|
||||
|
||||
def test_SimpleAddSettleWorkflow(self):
|
||||
alice_channel, bob_channel = self.alice_channel, self.bob_channel
|
||||
htlc = self.htlc
|
||||
|
||||
alice_out = alice_channel.current_commitment(LOCAL).outputs()
|
||||
short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42]
|
||||
long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62]
|
||||
self.assertLess(alice_out[long_idx].value, 5 * 10**8, alice_out)
|
||||
self.assertEqual(alice_out[short_idx].value, 5 * 10**8, alice_out)
|
||||
|
||||
alice_out = alice_channel.current_commitment(REMOTE).outputs()
|
||||
short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42]
|
||||
long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62]
|
||||
self.assertLess(alice_out[short_idx].value, 5 * 10**8)
|
||||
self.assertEqual(alice_out[long_idx].value, 5 * 10**8)
|
||||
|
||||
def com():
|
||||
return alice_channel.local_commitment
|
||||
|
||||
self.assertTrue(alice_channel.signature_fits(com()))
|
||||
|
||||
self.assertNotEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [])
|
||||
self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
|
||||
self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
self.assertNotEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [])
|
||||
self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
|
||||
|
||||
# this wouldn't work since we put None in the remote_sig
|
||||
# alice_channel.force_close_tx()
|
||||
from electrum.lnutil import extract_ctn_from_tx_and_chan
|
||||
tx0 = str(alice_channel.force_close_tx())
|
||||
self.assertEqual(alice_channel.config[LOCAL].ctn, 0)
|
||||
self.assertEqual(extract_ctn_from_tx_and_chan(alice_channel.force_close_tx(), alice_channel), 0)
|
||||
self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL)))
|
||||
|
||||
# Next alice commits this change by sending a signature message. Since
|
||||
# we expect the messages to be ordered, Bob will receive the HTLC we
|
||||
# just sent before he receives this signature, so the signature will
|
||||
# cover the HTLC.
|
||||
aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment()
|
||||
|
||||
self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature")
|
||||
|
||||
self.assertTrue(alice_channel.signature_fits(com()))
|
||||
self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com()))
|
||||
|
||||
self.assertEqual(next(iter(alice_channel.hm.pending_htlcs(REMOTE)))[0], RECEIVED)
|
||||
self.assertEqual(alice_channel.hm.pending_htlcs(REMOTE), bob_channel.hm.pending_htlcs(LOCAL))
|
||||
self.assertEqual(alice_channel.pending_commitment(REMOTE).outputs(), bob_channel.pending_commitment(LOCAL).outputs())
|
||||
|
||||
# Bob receives this signature message, and checks that this covers the
|
||||
# state he has in his remote log. This includes the HTLC just sent
|
||||
# from Alice.
|
||||
self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL)))
|
||||
bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)
|
||||
self.assertTrue(bob_channel.signature_fits(bob_channel.pending_commitment(LOCAL)))
|
||||
|
||||
self.assertEqual(bob_channel.config[REMOTE].ctn, 0)
|
||||
self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [htlc])
|
||||
|
||||
self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
|
||||
self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
|
@ -228,31 +300,68 @@ class TestChannel(unittest.TestCase):
|
|||
# Bob revokes his prior commitment given to him by Alice, since he now
|
||||
# has a valid signature for a newer commitment.
|
||||
bobRevocation, _ = bob_channel.revoke_current_commitment()
|
||||
bob_channel.serialize()
|
||||
self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL)))
|
||||
|
||||
# Bob finally send a signature for Alice's commitment transaction.
|
||||
# Bob finally sends a signature for Alice's commitment transaction.
|
||||
# This signature will cover the HTLC, since Bob will first send the
|
||||
# revocation just created. The revocation also acks every received
|
||||
# HTLC up to the point where Alice sent here signature.
|
||||
# HTLC up to the point where Alice sent her signature.
|
||||
bobSig, bobHtlcSigs = bob_channel.sign_next_commitment()
|
||||
self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL)))
|
||||
|
||||
self.assertEqual(len(bobHtlcSigs), 1)
|
||||
|
||||
self.assertTrue(alice_channel.signature_fits(com()))
|
||||
self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com()))
|
||||
|
||||
self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 3)
|
||||
|
||||
# Alice then processes this revocation, sending her own revocation for
|
||||
# her prior commitment transaction. Alice shouldn't have any HTLCs to
|
||||
# forward since she's sending an outgoing HTLC.
|
||||
alice_channel.receive_revocation(bobRevocation)
|
||||
|
||||
# test serializing with locked_in htlc
|
||||
self.assertEqual(len(alice_channel.to_save()['local_log']), 1)
|
||||
alice_channel.serialize()
|
||||
self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs())
|
||||
|
||||
self.assertTrue(alice_channel.signature_fits(com()))
|
||||
self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL)))
|
||||
alice_channel.serialize()
|
||||
self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com()))
|
||||
|
||||
self.assertEqual(len(alice_channel.current_commitment(LOCAL).outputs()), 2)
|
||||
self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3)
|
||||
self.assertEqual(len(com().outputs()), 2)
|
||||
self.assertEqual(len(alice_channel.force_close_tx().outputs()), 2)
|
||||
|
||||
self.assertEqual(alice_channel.hm.log.keys(), set([LOCAL, REMOTE]))
|
||||
self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1)
|
||||
alice_channel.serialize()
|
||||
|
||||
self.assertEqual(alice_channel.pending_commitment(LOCAL).outputs(),
|
||||
bob_channel.pending_commitment(REMOTE).outputs())
|
||||
|
||||
# Alice then processes bob's signature, and since she just received
|
||||
# the revocation, she expect this signature to cover everything up to
|
||||
# the point where she sent her signature, including the HTLC.
|
||||
alice_channel.receive_new_commitment(bobSig, bobHtlcSigs)
|
||||
self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs())
|
||||
|
||||
self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3)
|
||||
self.assertEqual(len(com().outputs()), 3)
|
||||
self.assertEqual(len(alice_channel.force_close_tx().outputs()), 3)
|
||||
|
||||
self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1)
|
||||
alice_channel.serialize()
|
||||
|
||||
tx1 = str(alice_channel.force_close_tx())
|
||||
self.assertNotEqual(tx0, tx1)
|
||||
|
||||
# Alice then generates a revocation for bob.
|
||||
self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs())
|
||||
aliceRevocation, _ = alice_channel.revoke_current_commitment()
|
||||
alice_channel.serialize()
|
||||
#self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs())
|
||||
|
||||
tx2 = str(alice_channel.force_close_tx())
|
||||
# since alice already has the signature for the next one, it doesn't change her force close tx (it was already the newer one)
|
||||
|
@ -262,7 +371,9 @@ class TestChannel(unittest.TestCase):
|
|||
# is fully locked in within both commitment transactions. Bob should
|
||||
# also be able to forward an HTLC now that the HTLC has been locked
|
||||
# into both commitment transactions.
|
||||
self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL)))
|
||||
bob_channel.receive_revocation(aliceRevocation)
|
||||
bob_channel.serialize()
|
||||
|
||||
# At this point, both sides should have the proper number of satoshis
|
||||
# sent, and commitment height updated within their local channel
|
||||
|
@ -279,16 +390,19 @@ class TestChannel(unittest.TestCase):
|
|||
|
||||
# Both commitment transactions should have three outputs, and one of
|
||||
# them should be exactly the amount of the HTLC.
|
||||
self.assertEqual(len(alice_channel.local_commitment.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_channel.local_commitment.outputs()))
|
||||
self.assertEqual(len(bob_channel.local_commitment.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_channel.local_commitment.outputs()))
|
||||
self.assertOutputExistsByValue(alice_channel.local_commitment, htlc.amount_msat // 1000)
|
||||
self.assertOutputExistsByValue(bob_channel.local_commitment, htlc.amount_msat // 1000)
|
||||
alice_ctx = alice_channel.pending_commitment(LOCAL)
|
||||
bob_ctx = bob_channel.pending_commitment(LOCAL)
|
||||
self.assertEqual(len(alice_ctx.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_ctx.outputs()))
|
||||
self.assertEqual(len(bob_ctx.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_ctx.outputs()))
|
||||
self.assertOutputExistsByValue(alice_ctx, htlc.amount_msat // 1000)
|
||||
self.assertOutputExistsByValue(bob_ctx, htlc.amount_msat // 1000)
|
||||
|
||||
# Now we'll repeat a similar exchange, this time with Bob settling the
|
||||
# HTLC once he learns of the preimage.
|
||||
preimage = self.paymentPreimage
|
||||
bob_channel.settle_htlc(preimage, self.bobHtlcIndex)
|
||||
|
||||
#self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs())
|
||||
alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex)
|
||||
|
||||
tx3 = str(alice_channel.force_close_tx())
|
||||
|
@ -296,28 +410,43 @@ class TestChannel(unittest.TestCase):
|
|||
self.assertEqual(tx2, tx3)
|
||||
|
||||
bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()
|
||||
self.assertEqual(len(bobHtlcSigs2), 0)
|
||||
|
||||
self.assertEqual(alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED), [htlc])
|
||||
self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, alice_channel.config[REMOTE].ctn), [htlc])
|
||||
self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
|
||||
self.assertEqual({1: [htlc], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
self.assertEqual({1: [], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
|
||||
self.assertEqual({1: [], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
|
||||
|
||||
alice_ctx_bob_version = bob_channel.pending_commitment(REMOTE).outputs()
|
||||
alice_ctx_alice_version = alice_channel.pending_commitment(LOCAL).outputs()
|
||||
self.assertEqual(alice_ctx_alice_version, alice_ctx_bob_version)
|
||||
|
||||
alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)
|
||||
|
||||
tx4 = str(alice_channel.force_close_tx())
|
||||
self.assertNotEqual(tx3, tx4)
|
||||
|
||||
self.assertEqual(alice_channel.balance(LOCAL), 500000000000)
|
||||
self.assertEqual(1, alice_channel.config[LOCAL].ctn)
|
||||
self.assertEqual(len(alice_channel.included_htlcs(LOCAL, RECEIVED, ctn=2)), 0)
|
||||
aliceRevocation2, _ = alice_channel.revoke_current_commitment()
|
||||
alice_channel.serialize()
|
||||
aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment()
|
||||
self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures")
|
||||
|
||||
self.assertEqual(len(bob_channel.current_commitment(LOCAL).outputs()), 3)
|
||||
self.assertEqual(len(bob_channel.pending_commitment(LOCAL).outputs()), 2)
|
||||
received, sent = bob_channel.receive_revocation(aliceRevocation2)
|
||||
bob_channel.serialize()
|
||||
self.assertEqual(received, one_bitcoin_in_msat)
|
||||
|
||||
bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)
|
||||
|
||||
bobRevocation2, _ = bob_channel.revoke_current_commitment()
|
||||
bob_channel.serialize()
|
||||
alice_channel.receive_revocation(bobRevocation2)
|
||||
alice_channel.serialize()
|
||||
|
||||
# At this point, Bob should have 6 BTC settled, with Alice still having
|
||||
# 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel
|
||||
|
@ -331,15 +460,15 @@ class TestChannel(unittest.TestCase):
|
|||
self.assertEqual(bob_channel.current_height[LOCAL], 2, "bob has incorrect commitment height")
|
||||
self.assertEqual(alice_channel.current_height[LOCAL], 2, "alice has incorrect commitment height")
|
||||
|
||||
# The logs of both sides should now be cleared since the entry adding
|
||||
# the HTLC should have been removed once both sides receive the
|
||||
# revocation.
|
||||
#self.assertEqual(alice_channel.local_update_log, [], "alice's local not updated, should be empty, has %s entries instead"% len(alice_channel.local_update_log))
|
||||
#self.assertEqual(alice_channel.remote_update_log, [], "alice's remote not updated, should be empty, has %s entries instead"% len(alice_channel.remote_update_log))
|
||||
self.assertEqual(self.bob_pending_remote_balance, self.alice_channel.balance(LOCAL))
|
||||
|
||||
alice_channel.update_fee(100000, True)
|
||||
alice_outputs = alice_channel.pending_commitment(REMOTE).outputs()
|
||||
old_outputs = bob_channel.pending_commitment(LOCAL).outputs()
|
||||
bob_channel.update_fee(100000, False)
|
||||
new_outputs = bob_channel.pending_commitment(LOCAL).outputs()
|
||||
self.assertNotEqual(old_outputs, new_outputs)
|
||||
self.assertEqual(alice_outputs, new_outputs)
|
||||
|
||||
tx5 = str(alice_channel.force_close_tx())
|
||||
# sending a fee update does not change her force close tx
|
||||
|
@ -353,10 +482,17 @@ class TestChannel(unittest.TestCase):
|
|||
self.htlc_dict['amount_msat'] *= 5
|
||||
bob_index = bob_channel.add_htlc(self.htlc_dict)
|
||||
alice_index = alice_channel.receive_htlc(self.htlc_dict)
|
||||
force_state_transition(alice_channel, bob_channel)
|
||||
|
||||
bob_channel.pending_commitment(REMOTE)
|
||||
alice_channel.pending_commitment(LOCAL)
|
||||
|
||||
alice_channel.pending_commitment(REMOTE)
|
||||
bob_channel.pending_commitment(LOCAL)
|
||||
|
||||
force_state_transition(bob_channel, alice_channel)
|
||||
alice_channel.settle_htlc(self.paymentPreimage, alice_index)
|
||||
bob_channel.receive_htlc_settle(self.paymentPreimage, bob_index)
|
||||
force_state_transition(alice_channel, bob_channel)
|
||||
force_state_transition(bob_channel, alice_channel)
|
||||
self.assertEqual(alice_channel.total_msat(SENT), one_bitcoin_in_msat, "alice satoshis sent incorrect")
|
||||
self.assertEqual(alice_channel.total_msat(RECEIVED), 5 * one_bitcoin_in_msat, "alice satoshis received incorrect")
|
||||
self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, "bob satoshis received incorrect")
|
||||
|
@ -366,8 +502,15 @@ class TestChannel(unittest.TestCase):
|
|||
|
||||
|
||||
def alice_to_bob_fee_update(self, fee=111):
|
||||
aoldctx = self.alice_channel.pending_commitment(REMOTE).outputs()
|
||||
self.alice_channel.update_fee(fee, True)
|
||||
anewctx = self.alice_channel.pending_commitment(REMOTE).outputs()
|
||||
self.assertNotEqual(aoldctx, anewctx)
|
||||
boldctx = self.bob_channel.pending_commitment(LOCAL).outputs()
|
||||
self.bob_channel.update_fee(fee, False)
|
||||
bnewctx = self.bob_channel.pending_commitment(LOCAL).outputs()
|
||||
self.assertNotEqual(boldctx, bnewctx)
|
||||
self.assertEqual(anewctx, bnewctx)
|
||||
return fee
|
||||
|
||||
def test_UpdateFeeSenderCommits(self):
|
||||
|
@ -444,7 +587,7 @@ class TestChannel(unittest.TestCase):
|
|||
# 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
|
||||
new['amount_msat'] *= 2.5
|
||||
new['payment_hash'] = bitcoin.sha256(32 * b'\x04')
|
||||
with self.assertRaises(lnutil.PaymentFailure) as cm:
|
||||
self.alice_channel.add_htlc(new)
|
||||
|
@ -462,7 +605,6 @@ class TestChannel(unittest.TestCase):
|
|||
except:
|
||||
try:
|
||||
from deepdiff import DeepDiff
|
||||
from pprint import pformat
|
||||
except ImportError:
|
||||
raise
|
||||
raise Exception(pformat(DeepDiff(before_signing, after_signing)))
|
||||
|
@ -549,9 +691,9 @@ class TestChanReserve(unittest.TestCase):
|
|||
force_state_transition(self.alice_channel, self.bob_channel)
|
||||
|
||||
aliceSelfBalance = self.alice_channel.balance(LOCAL)\
|
||||
- lnchan.htlcsum(self.alice_channel.htlcs(LOCAL, True))
|
||||
- lnchan.htlcsum(self.alice_channel.hm.htlcs_by_direction(LOCAL, SENT))
|
||||
bobBalance = self.bob_channel.balance(REMOTE)\
|
||||
- lnchan.htlcsum(self.alice_channel.htlcs(REMOTE, True))
|
||||
- lnchan.htlcsum(self.alice_channel.hm.htlcs_by_direction(REMOTE, SENT))
|
||||
self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5)
|
||||
self.assertEqual(bobBalance, one_bitcoin_in_msat*5)
|
||||
# Now let Bob try to add an HTLC. This should fail, since it will
|
||||
|
@ -647,17 +789,22 @@ class TestDust(unittest.TestCase):
|
|||
'cltv_expiry' : 5, # also in create_test_channels
|
||||
}
|
||||
|
||||
old_values = [x.value for x in bob_channel.current_commitment(LOCAL).outputs() ]
|
||||
aliceHtlcIndex = alice_channel.add_htlc(htlc)
|
||||
bobHtlcIndex = bob_channel.receive_htlc(htlc)
|
||||
force_state_transition(alice_channel, bob_channel)
|
||||
self.assertEqual(len(alice_channel.local_commitment.outputs()), 3)
|
||||
self.assertEqual(len(bob_channel.local_commitment.outputs()), 2)
|
||||
alice_ctx = alice_channel.current_commitment(LOCAL)
|
||||
bob_ctx = bob_channel.current_commitment(LOCAL)
|
||||
new_values = [x.value for x in bob_ctx.outputs() ]
|
||||
self.assertNotEqual(old_values, new_values)
|
||||
self.assertEqual(len(alice_ctx.outputs()), 3)
|
||||
self.assertEqual(len(bob_ctx.outputs()), 2)
|
||||
default_fee = calc_static_fee(0)
|
||||
self.assertEqual(bob_channel.pending_local_fee(), default_fee + htlcAmt)
|
||||
bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex)
|
||||
alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex)
|
||||
force_state_transition(bob_channel, alice_channel)
|
||||
self.assertEqual(len(alice_channel.local_commitment.outputs()), 2)
|
||||
self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 2)
|
||||
self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt)
|
||||
|
||||
def force_state_transition(chanA, chanB):
|
||||
|
|
95
electrum/tests/test_lnhtlc.py
Normal file
95
electrum/tests/test_lnhtlc.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
import unittest
|
||||
from electrum.lnutil import RECEIVED, LOCAL, REMOTE, SENT, HTLCOwner
|
||||
from electrum.lnhtlc import HTLCManager
|
||||
from typing import NamedTuple
|
||||
|
||||
class H(NamedTuple):
|
||||
owner : str
|
||||
htlc_id : int
|
||||
|
||||
class TestHTLCManager(unittest.TestCase):
|
||||
def test_race(self):
|
||||
A = HTLCManager()
|
||||
B = HTLCManager()
|
||||
ah0, bh0 = H('A', 0), H('B', 0)
|
||||
B.recv_htlc(A.send_htlc(ah0))
|
||||
self.assertTrue(B.expect_sig[RECEIVED])
|
||||
self.assertTrue(A.expect_sig[SENT])
|
||||
self.assertFalse(B.expect_sig[SENT])
|
||||
self.assertFalse(A.expect_sig[RECEIVED])
|
||||
self.assertEqual(B.log[REMOTE]['locked_in'][0][LOCAL], 1)
|
||||
A.recv_htlc(B.send_htlc(bh0))
|
||||
self.assertTrue(B.expect_sig[RECEIVED])
|
||||
self.assertTrue(A.expect_sig[SENT])
|
||||
self.assertTrue(A.expect_sig[SENT])
|
||||
self.assertTrue(B.expect_sig[RECEIVED])
|
||||
self.assertEqual(B.current_htlcs(LOCAL), [])
|
||||
self.assertEqual(A.current_htlcs(LOCAL), [])
|
||||
self.assertEqual(B.pending_htlcs(LOCAL), [(RECEIVED, ah0)])
|
||||
self.assertEqual(A.pending_htlcs(LOCAL), [(RECEIVED, bh0)])
|
||||
A.send_ctx()
|
||||
B.recv_ctx()
|
||||
B.send_ctx()
|
||||
A.recv_ctx()
|
||||
self.assertEqual(B.pending_htlcs(LOCAL), [(RECEIVED, ah0), (SENT, bh0)][::-1])
|
||||
self.assertEqual(A.pending_htlcs(LOCAL), [(RECEIVED, bh0), (SENT, ah0)][::-1])
|
||||
B.send_rev()
|
||||
A.recv_rev()
|
||||
A.send_rev()
|
||||
B.recv_rev()
|
||||
self.assertEqual(B.current_htlcs(LOCAL), [(RECEIVED, ah0), (SENT, bh0)][::-1])
|
||||
self.assertEqual(A.current_htlcs(LOCAL), [(RECEIVED, bh0), (SENT, ah0)][::-1])
|
||||
|
||||
def test_no_race(self):
|
||||
A = HTLCManager()
|
||||
B = HTLCManager()
|
||||
B.recv_htlc(A.send_htlc(H('A', 0)))
|
||||
self.assertEqual(len(B.pending_htlcs(REMOTE)), 1)
|
||||
A.send_ctx()
|
||||
B.recv_ctx()
|
||||
B.send_rev()
|
||||
A.recv_rev()
|
||||
B.send_ctx()
|
||||
A.recv_ctx()
|
||||
A.send_rev()
|
||||
B.recv_rev()
|
||||
self.assertEqual(len(A.current_htlcs(LOCAL)), 1)
|
||||
self.assertEqual(len(B.current_htlcs(LOCAL)), 1)
|
||||
B.send_settle(0)
|
||||
A.recv_settle(0)
|
||||
self.assertEqual(A.htlcs_by_direction(REMOTE, RECEIVED), [H('A', 0)])
|
||||
self.assertNotEqual(A.current_htlcs(LOCAL), [])
|
||||
self.assertNotEqual(B.current_htlcs(REMOTE), [])
|
||||
self.assertEqual(A.pending_htlcs(LOCAL), [])
|
||||
self.assertEqual(B.pending_htlcs(REMOTE), [])
|
||||
B.send_ctx()
|
||||
A.recv_ctx()
|
||||
A.send_rev()
|
||||
B.recv_rev()
|
||||
A.send_ctx()
|
||||
B.recv_ctx()
|
||||
B.send_rev()
|
||||
A.recv_rev()
|
||||
self.assertEqual(B.current_htlcs(LOCAL), [])
|
||||
self.assertEqual(A.current_htlcs(LOCAL), [])
|
||||
self.assertEqual(A.current_htlcs(REMOTE), [])
|
||||
self.assertEqual(B.current_htlcs(REMOTE), [])
|
||||
self.assertEqual(len(A.settled_htlcs(LOCAL)), 1)
|
||||
self.assertEqual(len(A.sent_in_ctn(2)), 1)
|
||||
self.assertEqual(len(B.received_in_ctn(2)), 1)
|
||||
|
||||
def test_settle_while_owing(self):
|
||||
A = HTLCManager()
|
||||
B = HTLCManager()
|
||||
B.recv_htlc(A.send_htlc(H('A', 0)))
|
||||
A.send_ctx()
|
||||
B.recv_ctx()
|
||||
B.send_rev()
|
||||
A.recv_rev()
|
||||
B.send_settle(0)
|
||||
A.recv_settle(0)
|
||||
self.assertEqual(B.pending_htlcs(REMOTE), [])
|
||||
B.send_ctx()
|
||||
A.recv_ctx()
|
||||
A.send_rev()
|
||||
B.recv_rev()
|
|
@ -6,8 +6,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
|
|||
make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
|
||||
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
|
||||
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
|
||||
ScriptHtlc, extract_nodeid, calc_onchain_fees)
|
||||
from electrum import lnchan
|
||||
ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc)
|
||||
from electrum.util import bh2u, bfh
|
||||
from electrum.transaction import Transaction
|
||||
|
||||
|
@ -496,7 +495,7 @@ class TestLNUtil(unittest.TestCase):
|
|||
(1, 2000 * 1000),
|
||||
(3, 3000 * 1000),
|
||||
(4, 4000 * 1000)]:
|
||||
htlc_obj[num] = lnchan.UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=None, htlc_id=None)
|
||||
htlc_obj[num] = UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=None, htlc_id=None)
|
||||
htlcs = [ScriptHtlc(htlc[x], htlc_obj[x]) for x in range(5)]
|
||||
|
||||
our_commit_tx = make_commitment(
|
||||
|
@ -506,7 +505,7 @@ class TestLNUtil(unittest.TestCase):
|
|||
local_revocation_pubkey, local_delayedpubkey, local_delay,
|
||||
funding_tx_id, funding_output_index, funding_amount_satoshi,
|
||||
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
|
||||
calc_onchain_fees(len(htlcs), local_feerate_per_kw, True, we_are_initiator=True), htlcs=htlcs)
|
||||
calc_onchain_fees(len(htlcs), local_feerate_per_kw, True), htlcs=htlcs)
|
||||
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
|
||||
self.assertEqual(str(our_commit_tx), output_commit_tx)
|
||||
|
||||
|
@ -584,7 +583,7 @@ class TestLNUtil(unittest.TestCase):
|
|||
local_revocation_pubkey, local_delayedpubkey, local_delay,
|
||||
funding_tx_id, funding_output_index, funding_amount_satoshi,
|
||||
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True, we_are_initiator=True), htlcs=[])
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True), htlcs=[])
|
||||
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
|
||||
|
||||
self.assertEqual(str(our_commit_tx), output_commit_tx)
|
||||
|
@ -603,7 +602,7 @@ class TestLNUtil(unittest.TestCase):
|
|||
local_revocation_pubkey, local_delayedpubkey, local_delay,
|
||||
funding_tx_id, funding_output_index, funding_amount_satoshi,
|
||||
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True, we_are_initiator=True), htlcs=[])
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True), htlcs=[])
|
||||
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
|
||||
|
||||
self.assertEqual(str(our_commit_tx), output_commit_tx)
|
||||
|
@ -661,7 +660,7 @@ class TestLNUtil(unittest.TestCase):
|
|||
local_revocation_pubkey, local_delayedpubkey, local_delay,
|
||||
funding_tx_id, funding_output_index, funding_amount_satoshi,
|
||||
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True, we_are_initiator=True), htlcs=[])
|
||||
calc_onchain_fees(0, local_feerate_per_kw, True), htlcs=[])
|
||||
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
|
||||
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
|
||||
self.assertEqual(str(our_commit_tx), ref_commit_tx_str)
|
||||
|
|
Loading…
Add table
Reference in a new issue