commit 1763d02b05fca7c27ef7892271110a6853821e5e
parent b26dc66567fe40f5623d274a47b131893e368ee9
Author: Janus <ysangkok@gmail.com>
Date: Thu, 11 Oct 2018 17:15:25 +0200
rename lnhtlc->lnchan, HTLCStateMachine->Channel
Diffstat:
8 files changed, 1188 insertions(+), 1188 deletions(-)
diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
@@ -5,7 +5,7 @@ from PyQt5.QtWidgets import *
from electrum.util import inv_dict, bh2u, bfh
from electrum.i18n import _
-from electrum.lnhtlc import HTLCStateMachine
+from electrum.lnchan import Channel
from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
@@ -13,7 +13,7 @@ from .amountedit import BTCAmountEdit
class ChannelsList(MyTreeWidget):
update_rows = QtCore.pyqtSignal()
- update_single_row = QtCore.pyqtSignal(HTLCStateMachine)
+ update_single_row = QtCore.pyqtSignal(Channel)
def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Node ID'), _('Balance'), _('Remote'), _('Status')], 0)
@@ -43,7 +43,7 @@ class ChannelsList(MyTreeWidget):
menu.addAction(_("Force-close channel"), close)
menu.exec_(self.viewport().mapToGlobal(position))
- @QtCore.pyqtSlot(HTLCStateMachine)
+ @QtCore.pyqtSlot(Channel)
def do_update_single_row(self, chan):
for i in range(self.topLevelItemCount()):
item = self.topLevelItem(i)
diff --git a/electrum/lnbase.py b/electrum/lnbase.py
@@ -27,7 +27,7 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions
from .transaction import Transaction, TxOutput
from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, OnionFailureCode
from .lnaddr import lndecode
-from .lnhtlc import HTLCStateMachine, RevokeAndAck, htlcsum
+from .lnchan import Channel, RevokeAndAck, htlcsum
from .lnutil import (Outpoint, LocalConfig, ChannelConfig,
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
funding_output_script, get_ecdh, get_per_commitment_secret_from_seed,
@@ -641,7 +641,7 @@ class Peer(PrintError):
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate),
"remote_commitment_to_be_revoked": None,
}
- m = HTLCStateMachine(chan)
+ m = Channel(chan)
m.lnwatcher = self.lnwatcher
m.sweep_address = self.lnworker.sweep_address
sig_64, _ = m.sign_next_commitment()
@@ -737,7 +737,7 @@ class Peer(PrintError):
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate),
"remote_commitment_to_be_revoked": None,
}
- m = HTLCStateMachine(chan)
+ m = Channel(chan)
m.lnwatcher = self.lnwatcher
m.sweep_address = self.lnworker.sweep_address
remote_sig = funding_created['signature']
diff --git a/electrum/lnchan.py b/electrum/lnchan.py
@@ -0,0 +1,810 @@
+# ported from lnd 42de4400bff5105352d0552155f73589166d162b
+from collections import namedtuple, defaultdict
+import binascii
+import json
+from enum import Enum, auto
+from typing import Optional
+
+from .util import bfh, PrintError, bh2u
+from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS
+from .bitcoin import redeem_script_to_address
+from .crypto import sha256
+from . import ecc
+from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction
+from .lnutil import get_per_commitment_secret_from_seed
+from .lnutil import make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script
+from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey
+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_outputs
+from .lnutil import ScriptHtlc, SENT, RECEIVED
+from .transaction import Transaction, TxOutput, construct_witness
+from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
+
+
+RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
+
+class FeeUpdateProgress(Enum):
+ FUNDEE_SIGNED = auto()
+ FUNDEE_ACKED = auto()
+ FUNDER_SIGNED = auto()
+
+FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED
+FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED
+FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED
+
+class FeeUpdate(defaultdict):
+ def __init__(self, chan, rate):
+ super().__init__(lambda: False)
+ self.rate = rate
+ self.chan = chan
+
+ def pending_feerate(self, subject):
+ if self[FUNDEE_ACKED]:
+ return self.rate
+ if subject == REMOTE and self.chan.constraints.is_initiator:
+ return self.rate
+ if subject == LOCAL and not self.chan.constraints.is_initiator:
+ return self.rate
+ # implicit return None
+
+class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'locked_in', 'htlc_id'])):
+ __slots__ = ()
+ def __new__(cls, *args, **kwargs):
+ if len(args) > 0:
+ args = list(args)
+ if type(args[1]) is str:
+ args[1] = bfh(args[1])
+ args[3] = {HTLCOwner(int(x)): y for x,y in args[3].items()}
+ return super().__new__(cls, *args)
+ if type(kwargs['payment_hash']) is str:
+ kwargs['payment_hash'] = bfh(kwargs['payment_hash'])
+ if 'locked_in' not in kwargs:
+ kwargs['locked_in'] = {LOCAL: None, REMOTE: None}
+ else:
+ kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()}
+ return super().__new__(cls, **kwargs)
+
+def decodeAll(d, local):
+ for k, v in d.items():
+ if k == 'revocation_store':
+ yield (k, RevocationStore.from_json_obj(v))
+ elif k.endswith("_basepoint") or k.endswith("_key"):
+ if local:
+ yield (k, Keypair(**dict(decodeAll(v, local))))
+ else:
+ yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local))))
+ elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None:
+ yield (k, binascii.unhexlify(v))
+ else:
+ yield (k, v)
+
+def htlcsum(htlcs):
+ return sum([x.amount_msat for x in htlcs])
+
+class Channel(PrintError):
+ def diagnostic_name(self):
+ return str(self.name)
+
+ def __init__(self, state, name = None):
+ assert 'local_state' not in state
+ self.config = {}
+ self.config[LOCAL] = state["local_config"]
+ if type(self.config[LOCAL]) is not LocalConfig:
+ conf = dict(decodeAll(self.config[LOCAL], True))
+ self.config[LOCAL] = LocalConfig(**conf)
+ assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes
+
+ self.config[REMOTE] = state["remote_config"]
+ if type(self.config[REMOTE]) is not RemoteConfig:
+ conf = dict(decodeAll(self.config[REMOTE], False))
+ self.config[REMOTE] = RemoteConfig(**conf)
+ assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes
+
+ self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"]
+ self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"]
+ self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
+ self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"]
+ self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"]
+ self.short_channel_id_predicted = self.short_channel_id
+ self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {}
+
+ # FIXME this is a tx serialised in the custom electrum partial tx format.
+ # 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"])
+
+ template = lambda: {'adds': {}, 'settles': []}
+ self.log = {LOCAL: template(), REMOTE: template()}
+ 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.name = name
+
+ self.fee_mgr = []
+
+ self.local_commitment = self.pending_local_commitment
+ self.remote_commitment = self.pending_remote_commitment
+
+ self._is_funding_txo_spent = None # "don't know"
+ self.set_state('DISCONNECTED')
+
+ self.lnwatcher = None
+
+ self.settled = {LOCAL: state.get('settled_local', []), REMOTE: state.get('settled_remote', [])}
+
+ def set_state(self, state: str):
+ self._state = state
+
+ def get_state(self):
+ return self._state
+
+ def set_funding_txo_spentness(self, is_spent: bool):
+ assert isinstance(is_spent, bool)
+ self._is_funding_txo_spent = is_spent
+
+ def should_try_to_reestablish_peer(self) -> bool:
+ return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED'
+
+ def get_funding_address(self):
+ script = funding_output_script(self.config[LOCAL], self.config[REMOTE])
+ return redeem_script_to_address('p2wsh', script)
+
+ def add_htlc(self, htlc):
+ """
+ AddHTLC adds an HTLC to the state machine's local update log. This method
+ should be called when preparing to send an outgoing HTLC.
+ """
+ assert type(htlc) is dict
+ htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id)
+ self.log[LOCAL]['adds'][htlc.htlc_id] = htlc
+ self.print_error("add_htlc")
+ self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1)
+ return htlc.htlc_id
+
+ def receive_htlc(self, htlc):
+ """
+ ReceiveHTLC adds an HTLC to the state machine's remote update log. This
+ method should be called in response to receiving a new HTLC from the remote
+ party.
+ """
+ assert type(htlc) is dict
+ htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id)
+ self.log[REMOTE]['adds'][htlc.htlc_id] = htlc
+ self.print_error("receive_htlc")
+ self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1)
+ return htlc.htlc_id
+
+ def sign_next_commitment(self):
+ """
+ SignNextCommitment signs a new commitment which includes any previous
+ unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs
+ committed in previous commitment updates. Signing a new commitment
+ decrements the available revocation window by 1. After a successful method
+ call, the remote party's commitment chain is extended by a new commitment
+ which includes all updates to the HTLC log prior to this method invocation.
+ The first return parameter is the signature for the commitment transaction
+ itself, while the second parameter is a slice of all HTLC signatures (if
+ any). The HTLC signatures are sorted according to the BIP 69 order of the
+ HTLC's on the commitment transaction.
+ """
+ for htlc in self.log[LOCAL]['adds'].values():
+ if htlc.locked_in[LOCAL] is None:
+ htlc.locked_in[LOCAL] = self.config[LOCAL].ctn
+ self.print_error("sign_next_commitment")
+
+ pending_remote_commitment = self.pending_remote_commitment
+ sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
+
+ their_remote_htlc_privkey_number = derive_privkey(
+ int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'),
+ self.config[REMOTE].next_per_commitment_point)
+ their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
+
+ for_us = False
+
+ htlcsigs = []
+ for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]):
+ for htlc in htlcs:
+ args = [self.config[REMOTE].next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc]
+ htlc_tx = make_htlc_tx_with_open_channel(self, *args)
+ sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
+ htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
+ htlcsigs.append((pending_remote_commitment.htlc_output_indices[htlc.payment_hash], htlc_sig))
+
+ for pending_fee in self.fee_mgr:
+ if not self.constraints.is_initiator:
+ pending_fee[FUNDEE_SIGNED] = True
+ if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]:
+ pending_fee[FUNDER_SIGNED] = True
+
+ self.process_new_offchain_ctx(pending_remote_commitment, ours=False)
+
+ htlcsigs.sort()
+ htlcsigs = [x[1] for x in htlcsigs]
+
+ return sig_64, htlcsigs
+
+ def receive_new_commitment(self, sig, htlc_sigs):
+ """
+ ReceiveNewCommitment process a signature for a new commitment state sent by
+ the remote party. This method should be called in response to the
+ remote party initiating a new change, or when the remote party sends a
+ signature fully accepting a new state we've initiated. If we are able to
+ successfully validate the signature, then the generated commitment is added
+ to our local commitment chain. Once we send a revocation for our prior
+ state, then this newly added commitment becomes our current accepted channel
+ state.
+ """
+
+ self.print_error("receive_new_commitment")
+ for htlc in self.log[REMOTE]['adds'].values():
+ if htlc.locked_in[REMOTE] is None:
+ htlc.locked_in[REMOTE] = self.config[REMOTE].ctn
+ assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
+
+ pending_local_commitment = self.pending_local_commitment
+ preimage_hex = pending_local_commitment.serialize_preimage(0)
+ pre_hash = Hash(bfh(preimage_hex))
+ if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash):
+ raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex)
+
+ _, this_point, _ = self.points
+
+ for htlcs, we_receive in [(self.included_htlcs(LOCAL, REMOTE), True), (self.included_htlcs(LOCAL, LOCAL), False)]:
+ for htlc in htlcs:
+ htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc)
+ pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
+ remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point)
+ for idx, sig in enumerate(htlc_sigs):
+ if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
+ del htlc_sigs[idx]
+ break
+ else:
+ raise Exception(f'failed verifying HTLC signatures: {htlc}')
+ if len(htlc_sigs) != 0: # all sigs should have been popped above
+ raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures')
+
+ for pending_fee in self.fee_mgr:
+ if not self.constraints.is_initiator:
+ pending_fee[FUNDEE_SIGNED] = True
+ if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]:
+ pending_fee[FUNDER_SIGNED] = True
+
+ self.process_new_offchain_ctx(pending_local_commitment, ours=True)
+
+
+ def revoke_current_commitment(self):
+ """
+ RevokeCurrentCommitment revokes the next lowest unrevoked commitment
+ transaction in the local commitment chain. As a result the edge of our
+ revocation window is extended by one, and the tail of our local commitment
+ chain is advanced by a single commitment. This now lowest unrevoked
+ commitment becomes our currently accepted state within the channel. This
+ method also returns the set of HTLC's currently active within the commitment
+ transaction. This return value allows callers to act once an HTLC has been
+ locked into our commitment transaction.
+ """
+ self.print_error("revoke_current_commitment")
+
+ last_secret, this_point, next_point = self.points
+
+ new_feerate = self.constraints.feerate
+
+ for pending_fee in self.fee_mgr[:]:
+ if not self.constraints.is_initiator and pending_fee[FUNDEE_SIGNED]:
+ new_feerate = pending_fee.rate
+ self.fee_mgr.remove(pending_fee)
+ print("FEERATE CHANGE COMPLETE (non-initiator)")
+ if self.constraints.is_initiator and pending_fee[FUNDER_SIGNED]:
+ new_feerate = pending_fee.rate
+ self.fee_mgr.remove(pending_fee)
+ print("FEERATE CHANGE COMPLETE (initiator)")
+
+ self.config[LOCAL]=self.config[LOCAL]._replace(
+ ctn=self.config[LOCAL].ctn + 1,
+ )
+ self.constraints=self.constraints._replace(
+ feerate=new_feerate
+ )
+
+ self.local_commitment = self.pending_local_commitment
+
+ return RevokeAndAck(last_secret, next_point), "current htlcs"
+
+ @property
+ def points(self):
+ last_small_num = self.config[LOCAL].ctn
+ this_small_num = last_small_num + 1
+ next_small_num = last_small_num + 2
+ last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num)
+ this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num)
+ 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
+
+ # TODO batch sweeps
+ # TODO sweep HTLC outputs
+ def process_new_offchain_ctx(self, ctx, ours: bool):
+ if not self.lnwatcher:
+ return
+ outpoint = self.funding_outpoint.to_str()
+ if ours:
+ ctn = self.config[LOCAL].ctn + 1
+ our_per_commitment_secret = get_per_commitment_secret_from_seed(
+ self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
+ our_cur_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
+ encumbered_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address)
+ else:
+ their_cur_pcp = self.config[REMOTE].next_per_commitment_point
+ encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address)
+ self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx)
+
+ def process_new_revocation_secret(self, per_commitment_secret: bytes):
+ if not self.lnwatcher:
+ return
+ outpoint = self.funding_outpoint.to_str()
+ ctx = self.remote_commitment_to_be_revoked
+ encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(self, ctx, per_commitment_secret, self.sweep_address)
+ self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx)
+
+ def receive_revocation(self, revocation):
+ """
+ ReceiveRevocation processes a revocation sent by the remote party for the
+ lowest unrevoked commitment within their commitment chain. We receive a
+ revocation either during the initial session negotiation wherein revocation
+ windows are extended, or in response to a state update that we initiate. If
+ successful, then the remote commitment chain is advanced by a single
+ commitment, and a log compaction is attempted.
+
+ Returns the forwarding package corresponding to the remote commitment height
+ that was revoked.
+ """
+ self.print_error("receive_revocation")
+
+ 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:
+ raise Exception('revoked secret not for current point')
+
+ # FIXME not sure this is correct... but it seems to work
+ # if there are update_add_htlc msgs between commitment_signed and rev_ack,
+ # this might break
+ prev_remote_commitment = self.pending_remote_commitment
+
+ self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret)
+ self.process_new_revocation_secret(revocation.per_commitment_secret)
+
+ 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)
+ 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)
+
+ next_point = self.config[REMOTE].next_per_commitment_point
+
+ print("RECEIVED", received_this_batch)
+ print("SENT", sent_this_batch)
+ 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)
+ )
+
+ for pending_fee in self.fee_mgr:
+ if self.constraints.is_initiator:
+ pending_fee[FUNDEE_ACKED] = True
+
+ self.local_commitment = self.pending_local_commitment
+ self.remote_commitment = self.pending_remote_commitment
+ self.remote_commitment_to_be_revoked = prev_remote_commitment
+ return received_this_batch, sent_this_batch
+
+ def balance(self, subject):
+ initial = self.config[subject].initial_msat
+
+ initial -= sum(self.settled[subject])
+ initial += sum(self.settled[-subject])
+
+ assert initial == self.config[subject].amount_msat
+ return initial
+
+ 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):
+ """
+ return filter of non-dust htlcs for subjects commitment transaction, initiated by given party
+ """
+ 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=True)
+ 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)
+
+ @property
+ def pending_remote_commitment(self):
+ this_point = self.config[REMOTE].next_per_commitment_point
+ return self.make_commitment(REMOTE, this_point)
+
+ def pending_feerate(self, subject):
+ candidate = self.constraints.feerate
+ for pending_fee in self.fee_mgr:
+ x = pending_fee.pending_feerate(subject)
+ if x is not None:
+ candidate = x
+
+ return candidate
+
+ @property
+ def pending_local_commitment(self):
+ _, this_point, _ = self.points
+ return self.make_commitment(LOCAL, this_point)
+
+ 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)
+ """
+ update_log = self.log[subject]
+ other_log = self.log[-subject]
+ res = []
+ for htlc in update_log['adds'].values():
+ locked_in = htlc.locked_in[subject]
+
+ if locked_in is None or only_pending == (htlc.htlc_id in other_log['settles']):
+ continue
+ res.append(htlc)
+ return res
+
+ def settle_htlc(self, preimage, htlc_id):
+ """
+ SettleHTLC attempts to settle an existing outstanding received HTLC.
+ """
+ self.print_error("settle_htlc")
+ htlc = self.log[REMOTE]['adds'][htlc_id]
+ assert htlc.payment_hash == sha256(preimage)
+ self.log[LOCAL]['settles'].append(htlc_id)
+
+ def receive_htlc_settle(self, preimage, htlc_index):
+ self.print_error("receive_htlc_settle")
+ htlc = self.log[LOCAL]['adds'][htlc_index]
+ assert htlc.payment_hash == sha256(preimage)
+ self.log[REMOTE]['settles'].append(htlc_index)
+
+ def receive_fail_htlc(self, htlc_id):
+ self.print_error("receive_fail_htlc")
+ self.log[LOCAL]['adds'].pop(htlc_id)
+
+ @property
+ def current_height(self):
+ return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn}
+
+ @property
+ def pending_local_fee(self):
+ return self.constraints.capacity - sum(x[2] for x in self.pending_local_commitment.outputs())
+
+ def update_fee(self, feerate):
+ if not self.constraints.is_initiator:
+ raise Exception("only initiator can update_fee, this counterparty is not initiator")
+ pending_fee = FeeUpdate(self, rate=feerate)
+ self.fee_mgr.append(pending_fee)
+
+ def receive_update_fee(self, feerate):
+ if self.constraints.is_initiator:
+ raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator")
+ pending_fee = FeeUpdate(self, rate=feerate)
+ self.fee_mgr.append(pending_fee)
+
+ 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 = []
+ for i in self.log[subject]['adds'].values():
+ locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None
+ 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],
+ "channel_id": self.channel_id,
+ "short_channel_id": self.short_channel_id,
+ "constraints": self.constraints,
+ "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,
+ "onion_keys": {str(k): bh2u(v) for k, v in self.onion_keys.items()},
+ "settled_local": self.settled[LOCAL],
+ "settled_remote": self.settled[REMOTE],
+ }
+
+ # 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):
+ namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()}
+ serialized_channel = {k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in self.to_save().items()}
+ class MyJsonEncoder(json.JSONEncoder):
+ def default(self, o):
+ if isinstance(o, bytes):
+ return binascii.hexlify(o).decode("ascii")
+ if isinstance(o, RevocationStore):
+ return o.serialize()
+ return super(MyJsonEncoder, self)
+ dumped = MyJsonEncoder().encode(serialized_channel)
+ roundtripped = json.loads(dumped)
+ reconstructed = Channel(roundtripped)
+ if reconstructed.to_save() != self.to_save():
+ from pprint import pformat
+ try:
+ from deepdiff import DeepDiff
+ except ImportError:
+ raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(reconstructed.to_save()) + "\n" + pformat(self.to_save()))
+ else:
+ raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(DeepDiff(reconstructed.to_save(), self.to_save())))
+ return roundtripped
+
+ def __str__(self):
+ return self.serialize()
+
+ def make_commitment(self, subject, this_point) -> Transaction:
+ remote_msat, local_msat = self.amounts()
+ assert local_msat >= 0
+ assert remote_msat >= 0
+ 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 = []
+ for htlc in self.included_htlcs(subject, -subject):
+ htlcs.append( ScriptHtlc( make_received_htlc(
+ other_revocation_pubkey,
+ other_htlc_pubkey,
+ this_htlc_pubkey,
+ htlc.payment_hash,
+ htlc.cltv_expiry), htlc))
+ for htlc in self.included_htlcs(subject, subject):
+ htlcs.append(
+ ScriptHtlc( make_offered_htlc(
+ other_revocation_pubkey,
+ other_htlc_pubkey,
+ this_htlc_pubkey,
+ htlc.payment_hash), htlc))
+ if subject != LOCAL:
+ remote_msat, local_msat = local_msat, remote_msat
+ payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
+ return make_commitment(
+ self.config[subject].ctn + 1,
+ this_config.multisig_key.pubkey,
+ other_config.multisig_key.pubkey,
+ payment_pubkey,
+ self.config[LOCAL].payment_basepoint.pubkey,
+ self.config[REMOTE].payment_basepoint.pubkey,
+ other_revocation_pubkey,
+ derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),
+ other_config.to_self_delay,
+ *self.funding_outpoint,
+ self.constraints.capacity,
+ local_msat,
+ remote_msat,
+ this_config.dust_limit_sat,
+ self.pending_feerate(subject),
+ subject == LOCAL,
+ self.constraints.is_initiator,
+ htlcs=htlcs)
+
+ def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int] = None) -> (bytes, int):
+ if fee_sat is None:
+ fee_sat = self.pending_local_fee
+
+ _, outputs = make_outputs(fee_sat * 1000, True,
+ self.config[LOCAL].amount_msat,
+ self.config[REMOTE].amount_msat,
+ (TYPE_SCRIPT, bh2u(local_script)),
+ (TYPE_SCRIPT, bh2u(remote_script)),
+ [], self.config[LOCAL].dust_limit_sat)
+
+ closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
+ self.config[REMOTE].multisig_key.pubkey,
+ self.config[LOCAL].payment_basepoint.pubkey,
+ self.config[REMOTE].payment_basepoint.pubkey,
+ # TODO hardcoded we_are_initiator:
+ True, *self.funding_outpoint, self.constraints.capacity,
+ outputs)
+
+ der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey))
+ sig = ecc.sig_string_from_der_sig(der_sig[:-1])
+ return sig, fee_sat
+
+def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
+ sweep_address) -> Optional[EncumberedTransaction]:
+ assert isinstance(their_pcp, bytes)
+ payment_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].payment_basepoint.privkey)
+ our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
+ our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
+ our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
+ to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
+ for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
+ if type_ == TYPE_ADDRESS and addr == to_remote_address:
+ break
+ else:
+ return None
+ sweep_tx = create_sweeptx_their_ctx_to_remote(address=sweep_address,
+ ctx=ctx,
+ output_idx=output_idx,
+ our_payment_privkey=our_payment_privkey)
+ return EncumberedTransaction(sweep_tx, csv_delay=0)
+
+
+def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret: bytes,
+ sweep_address) -> Optional[EncumberedTransaction]:
+ assert isinstance(per_commitment_secret, bytes)
+ per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
+ revocation_privkey = derive_blinded_privkey(chan.config[LOCAL].revocation_basepoint.privkey,
+ per_commitment_secret)
+ revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
+ to_self_delay = chan.config[LOCAL].to_self_delay
+ delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey,
+ per_commitment_point)
+ witness_script = bh2u(make_commitment_output_to_local_witness_script(
+ revocation_pubkey, to_self_delay, delayed_pubkey))
+ to_local_address = redeem_script_to_address('p2wsh', witness_script)
+ for output_idx, o in enumerate(ctx.outputs()):
+ if o.type == TYPE_ADDRESS and o.address == to_local_address:
+ break
+ else:
+ return None
+ sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
+ ctx=ctx,
+ output_idx=output_idx,
+ witness_script=witness_script,
+ privkey=revocation_privkey,
+ is_revocation=True)
+ return EncumberedTransaction(sweep_tx, csv_delay=0)
+
+
+def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
+ sweep_address) -> Optional[EncumberedTransaction]:
+ assert isinstance(our_pcp, bytes)
+ delayed_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].delayed_basepoint.privkey)
+ our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
+ our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
+ our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
+ revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey,
+ our_pcp)
+ to_self_delay = chan.config[REMOTE].to_self_delay
+ witness_script = bh2u(make_commitment_output_to_local_witness_script(
+ revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
+ to_local_address = redeem_script_to_address('p2wsh', witness_script)
+ for output_idx, o in enumerate(ctx.outputs()):
+ if o.type == TYPE_ADDRESS and o.address == to_local_address:
+ break
+ else:
+ return None
+ sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
+ ctx=ctx,
+ output_idx=output_idx,
+ witness_script=witness_script,
+ privkey=our_localdelayed_privkey.get_secret_bytes(),
+ is_revocation=False,
+ to_self_delay=to_self_delay)
+
+ return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay)
+
+
+def create_sweeptx_their_ctx_to_remote(address, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey,
+ fee_per_kb: int=None) -> Transaction:
+ our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
+ val = ctx.outputs()[output_idx].value
+ sweep_inputs = [{
+ 'type': 'p2wpkh',
+ 'x_pubkeys': [our_payment_pubkey],
+ 'num_sig': 1,
+ 'prevout_n': output_idx,
+ 'prevout_hash': ctx.txid(),
+ 'value': val,
+ 'coinbase': False,
+ 'signatures': [None],
+ }]
+ tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
+ if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
+ fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
+ sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val-fee)]
+ sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
+ sweep_tx.set_rbf(True)
+ sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
+ if not sweep_tx.is_complete():
+ raise Exception('channel close sweep tx is not complete')
+ return sweep_tx
+
+
+def create_sweeptx_ctx_to_local(address, ctx, output_idx: int, witness_script: str,
+ privkey: bytes, is_revocation: bool,
+ to_self_delay: int=None,
+ fee_per_kb: int=None) -> Transaction:
+ """Create a txn that sweeps the 'to_local' output of a commitment
+ transaction into our wallet.
+
+ privkey: either revocation_privkey or localdelayed_privkey
+ is_revocation: tells us which ^
+ """
+ val = ctx.outputs()[output_idx].value
+ sweep_inputs = [{
+ 'scriptSig': '',
+ 'type': 'p2wsh',
+ 'signatures': [],
+ 'num_sig': 0,
+ 'prevout_n': output_idx,
+ 'prevout_hash': ctx.txid(),
+ 'value': val,
+ 'coinbase': False,
+ 'preimage_script': witness_script,
+ }]
+ if to_self_delay is not None:
+ sweep_inputs[0]['sequence'] = to_self_delay
+ tx_size_bytes = 121 # approx size of to_local -> p2wpkh
+ if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
+ fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
+ sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val - fee)]
+ sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
+ sig = sweep_tx.sign_txin(0, privkey)
+ witness = construct_witness([sig, int(is_revocation), witness_script])
+ sweep_tx.inputs()[0]['witness'] = witness
+ return sweep_tx
diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py
@@ -1,810 +0,0 @@
-# ported from lnd 42de4400bff5105352d0552155f73589166d162b
-from collections import namedtuple, defaultdict
-import binascii
-import json
-from enum import Enum, auto
-from typing import Optional
-
-from .util import bfh, PrintError, bh2u
-from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS
-from .bitcoin import redeem_script_to_address
-from .crypto import sha256
-from . import ecc
-from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction
-from .lnutil import get_per_commitment_secret_from_seed
-from .lnutil import make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script
-from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey
-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_outputs
-from .lnutil import ScriptHtlc, SENT, RECEIVED
-from .transaction import Transaction, TxOutput, construct_witness
-from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
-
-
-RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
-
-class FeeUpdateProgress(Enum):
- FUNDEE_SIGNED = auto()
- FUNDEE_ACKED = auto()
- FUNDER_SIGNED = auto()
-
-FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED
-FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED
-FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED
-
-class FeeUpdate(defaultdict):
- def __init__(self, chan, rate):
- super().__init__(lambda: False)
- self.rate = rate
- self.chan = chan
-
- def pending_feerate(self, subject):
- if self[FUNDEE_ACKED]:
- return self.rate
- if subject == REMOTE and self.chan.constraints.is_initiator:
- return self.rate
- if subject == LOCAL and not self.chan.constraints.is_initiator:
- return self.rate
- # implicit return None
-
-class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'locked_in', 'htlc_id'])):
- __slots__ = ()
- def __new__(cls, *args, **kwargs):
- if len(args) > 0:
- args = list(args)
- if type(args[1]) is str:
- args[1] = bfh(args[1])
- args[3] = {HTLCOwner(int(x)): y for x,y in args[3].items()}
- return super().__new__(cls, *args)
- if type(kwargs['payment_hash']) is str:
- kwargs['payment_hash'] = bfh(kwargs['payment_hash'])
- if 'locked_in' not in kwargs:
- kwargs['locked_in'] = {LOCAL: None, REMOTE: None}
- else:
- kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()}
- return super().__new__(cls, **kwargs)
-
-def decodeAll(d, local):
- for k, v in d.items():
- if k == 'revocation_store':
- yield (k, RevocationStore.from_json_obj(v))
- elif k.endswith("_basepoint") or k.endswith("_key"):
- if local:
- yield (k, Keypair(**dict(decodeAll(v, local))))
- else:
- yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local))))
- elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None:
- yield (k, binascii.unhexlify(v))
- else:
- yield (k, v)
-
-def htlcsum(htlcs):
- return sum([x.amount_msat for x in htlcs])
-
-class HTLCStateMachine(PrintError):
- def diagnostic_name(self):
- return str(self.name)
-
- def __init__(self, state, name = None):
- assert 'local_state' not in state
- self.config = {}
- self.config[LOCAL] = state["local_config"]
- if type(self.config[LOCAL]) is not LocalConfig:
- conf = dict(decodeAll(self.config[LOCAL], True))
- self.config[LOCAL] = LocalConfig(**conf)
- assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes
-
- self.config[REMOTE] = state["remote_config"]
- if type(self.config[REMOTE]) is not RemoteConfig:
- conf = dict(decodeAll(self.config[REMOTE], False))
- self.config[REMOTE] = RemoteConfig(**conf)
- assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes
-
- self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"]
- self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"]
- self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
- self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"]
- self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"]
- self.short_channel_id_predicted = self.short_channel_id
- self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {}
-
- # FIXME this is a tx serialised in the custom electrum partial tx format.
- # 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"])
-
- template = lambda: {'adds': {}, 'settles': []}
- self.log = {LOCAL: template(), REMOTE: template()}
- 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.name = name
-
- self.fee_mgr = []
-
- self.local_commitment = self.pending_local_commitment
- self.remote_commitment = self.pending_remote_commitment
-
- self._is_funding_txo_spent = None # "don't know"
- self.set_state('DISCONNECTED')
-
- self.lnwatcher = None
-
- self.settled = {LOCAL: state.get('settled_local', []), REMOTE: state.get('settled_remote', [])}
-
- def set_state(self, state: str):
- self._state = state
-
- def get_state(self):
- return self._state
-
- def set_funding_txo_spentness(self, is_spent: bool):
- assert isinstance(is_spent, bool)
- self._is_funding_txo_spent = is_spent
-
- def should_try_to_reestablish_peer(self) -> bool:
- return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED'
-
- def get_funding_address(self):
- script = funding_output_script(self.config[LOCAL], self.config[REMOTE])
- return redeem_script_to_address('p2wsh', script)
-
- def add_htlc(self, htlc):
- """
- AddHTLC adds an HTLC to the state machine's local update log. This method
- should be called when preparing to send an outgoing HTLC.
- """
- assert type(htlc) is dict
- htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id)
- self.log[LOCAL]['adds'][htlc.htlc_id] = htlc
- self.print_error("add_htlc")
- self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1)
- return htlc.htlc_id
-
- def receive_htlc(self, htlc):
- """
- ReceiveHTLC adds an HTLC to the state machine's remote update log. This
- method should be called in response to receiving a new HTLC from the remote
- party.
- """
- assert type(htlc) is dict
- htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id)
- self.log[REMOTE]['adds'][htlc.htlc_id] = htlc
- self.print_error("receive_htlc")
- self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1)
- return htlc.htlc_id
-
- def sign_next_commitment(self):
- """
- SignNextCommitment signs a new commitment which includes any previous
- unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs
- committed in previous commitment updates. Signing a new commitment
- decrements the available revocation window by 1. After a successful method
- call, the remote party's commitment chain is extended by a new commitment
- which includes all updates to the HTLC log prior to this method invocation.
- The first return parameter is the signature for the commitment transaction
- itself, while the second parameter is a slice of all HTLC signatures (if
- any). The HTLC signatures are sorted according to the BIP 69 order of the
- HTLC's on the commitment transaction.
- """
- for htlc in self.log[LOCAL]['adds'].values():
- if htlc.locked_in[LOCAL] is None:
- htlc.locked_in[LOCAL] = self.config[LOCAL].ctn
- self.print_error("sign_next_commitment")
-
- pending_remote_commitment = self.pending_remote_commitment
- sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
-
- their_remote_htlc_privkey_number = derive_privkey(
- int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'),
- self.config[REMOTE].next_per_commitment_point)
- their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
-
- for_us = False
-
- htlcsigs = []
- for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]):
- for htlc in htlcs:
- args = [self.config[REMOTE].next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc]
- htlc_tx = make_htlc_tx_with_open_channel(self, *args)
- sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
- htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
- htlcsigs.append((pending_remote_commitment.htlc_output_indices[htlc.payment_hash], htlc_sig))
-
- for pending_fee in self.fee_mgr:
- if not self.constraints.is_initiator:
- pending_fee[FUNDEE_SIGNED] = True
- if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]:
- pending_fee[FUNDER_SIGNED] = True
-
- self.process_new_offchain_ctx(pending_remote_commitment, ours=False)
-
- htlcsigs.sort()
- htlcsigs = [x[1] for x in htlcsigs]
-
- return sig_64, htlcsigs
-
- def receive_new_commitment(self, sig, htlc_sigs):
- """
- ReceiveNewCommitment process a signature for a new commitment state sent by
- the remote party. This method should be called in response to the
- remote party initiating a new change, or when the remote party sends a
- signature fully accepting a new state we've initiated. If we are able to
- successfully validate the signature, then the generated commitment is added
- to our local commitment chain. Once we send a revocation for our prior
- state, then this newly added commitment becomes our current accepted channel
- state.
- """
-
- self.print_error("receive_new_commitment")
- for htlc in self.log[REMOTE]['adds'].values():
- if htlc.locked_in[REMOTE] is None:
- htlc.locked_in[REMOTE] = self.config[REMOTE].ctn
- assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
-
- pending_local_commitment = self.pending_local_commitment
- preimage_hex = pending_local_commitment.serialize_preimage(0)
- pre_hash = Hash(bfh(preimage_hex))
- if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash):
- raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex)
-
- _, this_point, _ = self.points
-
- for htlcs, we_receive in [(self.included_htlcs(LOCAL, REMOTE), True), (self.included_htlcs(LOCAL, LOCAL), False)]:
- for htlc in htlcs:
- htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc)
- pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
- remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point)
- for idx, sig in enumerate(htlc_sigs):
- if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
- del htlc_sigs[idx]
- break
- else:
- raise Exception(f'failed verifying HTLC signatures: {htlc}')
- if len(htlc_sigs) != 0: # all sigs should have been popped above
- raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures')
-
- for pending_fee in self.fee_mgr:
- if not self.constraints.is_initiator:
- pending_fee[FUNDEE_SIGNED] = True
- if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]:
- pending_fee[FUNDER_SIGNED] = True
-
- self.process_new_offchain_ctx(pending_local_commitment, ours=True)
-
-
- def revoke_current_commitment(self):
- """
- RevokeCurrentCommitment revokes the next lowest unrevoked commitment
- transaction in the local commitment chain. As a result the edge of our
- revocation window is extended by one, and the tail of our local commitment
- chain is advanced by a single commitment. This now lowest unrevoked
- commitment becomes our currently accepted state within the channel. This
- method also returns the set of HTLC's currently active within the commitment
- transaction. This return value allows callers to act once an HTLC has been
- locked into our commitment transaction.
- """
- self.print_error("revoke_current_commitment")
-
- last_secret, this_point, next_point = self.points
-
- new_feerate = self.constraints.feerate
-
- for pending_fee in self.fee_mgr[:]:
- if not self.constraints.is_initiator and pending_fee[FUNDEE_SIGNED]:
- new_feerate = pending_fee.rate
- self.fee_mgr.remove(pending_fee)
- print("FEERATE CHANGE COMPLETE (non-initiator)")
- if self.constraints.is_initiator and pending_fee[FUNDER_SIGNED]:
- new_feerate = pending_fee.rate
- self.fee_mgr.remove(pending_fee)
- print("FEERATE CHANGE COMPLETE (initiator)")
-
- self.config[LOCAL]=self.config[LOCAL]._replace(
- ctn=self.config[LOCAL].ctn + 1,
- )
- self.constraints=self.constraints._replace(
- feerate=new_feerate
- )
-
- self.local_commitment = self.pending_local_commitment
-
- return RevokeAndAck(last_secret, next_point), "current htlcs"
-
- @property
- def points(self):
- last_small_num = self.config[LOCAL].ctn
- this_small_num = last_small_num + 1
- next_small_num = last_small_num + 2
- last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num)
- this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num)
- 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
-
- # TODO batch sweeps
- # TODO sweep HTLC outputs
- def process_new_offchain_ctx(self, ctx, ours: bool):
- if not self.lnwatcher:
- return
- outpoint = self.funding_outpoint.to_str()
- if ours:
- ctn = self.config[LOCAL].ctn + 1
- our_per_commitment_secret = get_per_commitment_secret_from_seed(
- self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
- our_cur_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
- encumbered_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address)
- else:
- their_cur_pcp = self.config[REMOTE].next_per_commitment_point
- encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address)
- self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx)
-
- def process_new_revocation_secret(self, per_commitment_secret: bytes):
- if not self.lnwatcher:
- return
- outpoint = self.funding_outpoint.to_str()
- ctx = self.remote_commitment_to_be_revoked
- encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(self, ctx, per_commitment_secret, self.sweep_address)
- self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx)
-
- def receive_revocation(self, revocation):
- """
- ReceiveRevocation processes a revocation sent by the remote party for the
- lowest unrevoked commitment within their commitment chain. We receive a
- revocation either during the initial session negotiation wherein revocation
- windows are extended, or in response to a state update that we initiate. If
- successful, then the remote commitment chain is advanced by a single
- commitment, and a log compaction is attempted.
-
- Returns the forwarding package corresponding to the remote commitment height
- that was revoked.
- """
- self.print_error("receive_revocation")
-
- 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:
- raise Exception('revoked secret not for current point')
-
- # FIXME not sure this is correct... but it seems to work
- # if there are update_add_htlc msgs between commitment_signed and rev_ack,
- # this might break
- prev_remote_commitment = self.pending_remote_commitment
-
- self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret)
- self.process_new_revocation_secret(revocation.per_commitment_secret)
-
- 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)
- 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)
-
- next_point = self.config[REMOTE].next_per_commitment_point
-
- print("RECEIVED", received_this_batch)
- print("SENT", sent_this_batch)
- 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)
- )
-
- for pending_fee in self.fee_mgr:
- if self.constraints.is_initiator:
- pending_fee[FUNDEE_ACKED] = True
-
- self.local_commitment = self.pending_local_commitment
- self.remote_commitment = self.pending_remote_commitment
- self.remote_commitment_to_be_revoked = prev_remote_commitment
- return received_this_batch, sent_this_batch
-
- def balance(self, subject):
- initial = self.config[subject].initial_msat
-
- initial -= sum(self.settled[subject])
- initial += sum(self.settled[-subject])
-
- assert initial == self.config[subject].amount_msat
- return initial
-
- 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):
- """
- return filter of non-dust htlcs for subjects commitment transaction, initiated by given party
- """
- 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=True)
- 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)
-
- @property
- def pending_remote_commitment(self):
- this_point = self.config[REMOTE].next_per_commitment_point
- return self.make_commitment(REMOTE, this_point)
-
- def pending_feerate(self, subject):
- candidate = self.constraints.feerate
- for pending_fee in self.fee_mgr:
- x = pending_fee.pending_feerate(subject)
- if x is not None:
- candidate = x
-
- return candidate
-
- @property
- def pending_local_commitment(self):
- _, this_point, _ = self.points
- return self.make_commitment(LOCAL, this_point)
-
- 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)
- """
- update_log = self.log[subject]
- other_log = self.log[-subject]
- res = []
- for htlc in update_log['adds'].values():
- locked_in = htlc.locked_in[subject]
-
- if locked_in is None or only_pending == (htlc.htlc_id in other_log['settles']):
- continue
- res.append(htlc)
- return res
-
- def settle_htlc(self, preimage, htlc_id):
- """
- SettleHTLC attempts to settle an existing outstanding received HTLC.
- """
- self.print_error("settle_htlc")
- htlc = self.log[REMOTE]['adds'][htlc_id]
- assert htlc.payment_hash == sha256(preimage)
- self.log[LOCAL]['settles'].append(htlc_id)
-
- def receive_htlc_settle(self, preimage, htlc_index):
- self.print_error("receive_htlc_settle")
- htlc = self.log[LOCAL]['adds'][htlc_index]
- assert htlc.payment_hash == sha256(preimage)
- self.log[REMOTE]['settles'].append(htlc_index)
-
- def receive_fail_htlc(self, htlc_id):
- self.print_error("receive_fail_htlc")
- self.log[LOCAL]['adds'].pop(htlc_id)
-
- @property
- def current_height(self):
- return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn}
-
- @property
- def pending_local_fee(self):
- return self.constraints.capacity - sum(x[2] for x in self.pending_local_commitment.outputs())
-
- def update_fee(self, feerate):
- if not self.constraints.is_initiator:
- raise Exception("only initiator can update_fee, this counterparty is not initiator")
- pending_fee = FeeUpdate(self, rate=feerate)
- self.fee_mgr.append(pending_fee)
-
- def receive_update_fee(self, feerate):
- if self.constraints.is_initiator:
- raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator")
- pending_fee = FeeUpdate(self, rate=feerate)
- self.fee_mgr.append(pending_fee)
-
- 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 = []
- for i in self.log[subject]['adds'].values():
- locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None
- 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],
- "channel_id": self.channel_id,
- "short_channel_id": self.short_channel_id,
- "constraints": self.constraints,
- "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,
- "onion_keys": {str(k): bh2u(v) for k, v in self.onion_keys.items()},
- "settled_local": self.settled[LOCAL],
- "settled_remote": self.settled[REMOTE],
- }
-
- # 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):
- namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()}
- serialized_channel = {k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in self.to_save().items()}
- class MyJsonEncoder(json.JSONEncoder):
- def default(self, o):
- if isinstance(o, bytes):
- return binascii.hexlify(o).decode("ascii")
- if isinstance(o, RevocationStore):
- return o.serialize()
- return super(MyJsonEncoder, self)
- dumped = MyJsonEncoder().encode(serialized_channel)
- roundtripped = json.loads(dumped)
- reconstructed = HTLCStateMachine(roundtripped)
- if reconstructed.to_save() != self.to_save():
- from pprint import pformat
- try:
- from deepdiff import DeepDiff
- except ImportError:
- raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(reconstructed.to_save()) + "\n" + pformat(self.to_save()))
- else:
- raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(DeepDiff(reconstructed.to_save(), self.to_save())))
- return roundtripped
-
- def __str__(self):
- return self.serialize()
-
- def make_commitment(self, subject, this_point) -> Transaction:
- remote_msat, local_msat = self.amounts()
- assert local_msat >= 0
- assert remote_msat >= 0
- 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 = []
- for htlc in self.included_htlcs(subject, -subject):
- htlcs.append( ScriptHtlc( make_received_htlc(
- other_revocation_pubkey,
- other_htlc_pubkey,
- this_htlc_pubkey,
- htlc.payment_hash,
- htlc.cltv_expiry), htlc))
- for htlc in self.included_htlcs(subject, subject):
- htlcs.append(
- ScriptHtlc( make_offered_htlc(
- other_revocation_pubkey,
- other_htlc_pubkey,
- this_htlc_pubkey,
- htlc.payment_hash), htlc))
- if subject != LOCAL:
- remote_msat, local_msat = local_msat, remote_msat
- payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
- return make_commitment(
- self.config[subject].ctn + 1,
- this_config.multisig_key.pubkey,
- other_config.multisig_key.pubkey,
- payment_pubkey,
- self.config[LOCAL].payment_basepoint.pubkey,
- self.config[REMOTE].payment_basepoint.pubkey,
- other_revocation_pubkey,
- derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),
- other_config.to_self_delay,
- *self.funding_outpoint,
- self.constraints.capacity,
- local_msat,
- remote_msat,
- this_config.dust_limit_sat,
- self.pending_feerate(subject),
- subject == LOCAL,
- self.constraints.is_initiator,
- htlcs=htlcs)
-
- def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int] = None) -> (bytes, int):
- if fee_sat is None:
- fee_sat = self.pending_local_fee
-
- _, outputs = make_outputs(fee_sat * 1000, True,
- self.config[LOCAL].amount_msat,
- self.config[REMOTE].amount_msat,
- (TYPE_SCRIPT, bh2u(local_script)),
- (TYPE_SCRIPT, bh2u(remote_script)),
- [], self.config[LOCAL].dust_limit_sat)
-
- closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
- self.config[REMOTE].multisig_key.pubkey,
- self.config[LOCAL].payment_basepoint.pubkey,
- self.config[REMOTE].payment_basepoint.pubkey,
- # TODO hardcoded we_are_initiator:
- True, *self.funding_outpoint, self.constraints.capacity,
- outputs)
-
- der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey))
- sig = ecc.sig_string_from_der_sig(der_sig[:-1])
- return sig, fee_sat
-
-def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
- sweep_address) -> Optional[EncumberedTransaction]:
- assert isinstance(their_pcp, bytes)
- payment_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].payment_basepoint.privkey)
- our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
- our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
- our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
- to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
- for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
- if type_ == TYPE_ADDRESS and addr == to_remote_address:
- break
- else:
- return None
- sweep_tx = create_sweeptx_their_ctx_to_remote(address=sweep_address,
- ctx=ctx,
- output_idx=output_idx,
- our_payment_privkey=our_payment_privkey)
- return EncumberedTransaction(sweep_tx, csv_delay=0)
-
-
-def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret: bytes,
- sweep_address) -> Optional[EncumberedTransaction]:
- assert isinstance(per_commitment_secret, bytes)
- per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
- revocation_privkey = derive_blinded_privkey(chan.config[LOCAL].revocation_basepoint.privkey,
- per_commitment_secret)
- revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
- to_self_delay = chan.config[LOCAL].to_self_delay
- delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey,
- per_commitment_point)
- witness_script = bh2u(make_commitment_output_to_local_witness_script(
- revocation_pubkey, to_self_delay, delayed_pubkey))
- to_local_address = redeem_script_to_address('p2wsh', witness_script)
- for output_idx, o in enumerate(ctx.outputs()):
- if o.type == TYPE_ADDRESS and o.address == to_local_address:
- break
- else:
- return None
- sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
- ctx=ctx,
- output_idx=output_idx,
- witness_script=witness_script,
- privkey=revocation_privkey,
- is_revocation=True)
- return EncumberedTransaction(sweep_tx, csv_delay=0)
-
-
-def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
- sweep_address) -> Optional[EncumberedTransaction]:
- assert isinstance(our_pcp, bytes)
- delayed_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].delayed_basepoint.privkey)
- our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
- our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
- our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
- revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey,
- our_pcp)
- to_self_delay = chan.config[REMOTE].to_self_delay
- witness_script = bh2u(make_commitment_output_to_local_witness_script(
- revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
- to_local_address = redeem_script_to_address('p2wsh', witness_script)
- for output_idx, o in enumerate(ctx.outputs()):
- if o.type == TYPE_ADDRESS and o.address == to_local_address:
- break
- else:
- return None
- sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
- ctx=ctx,
- output_idx=output_idx,
- witness_script=witness_script,
- privkey=our_localdelayed_privkey.get_secret_bytes(),
- is_revocation=False,
- to_self_delay=to_self_delay)
-
- return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay)
-
-
-def create_sweeptx_their_ctx_to_remote(address, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey,
- fee_per_kb: int=None) -> Transaction:
- our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
- val = ctx.outputs()[output_idx].value
- sweep_inputs = [{
- 'type': 'p2wpkh',
- 'x_pubkeys': [our_payment_pubkey],
- 'num_sig': 1,
- 'prevout_n': output_idx,
- 'prevout_hash': ctx.txid(),
- 'value': val,
- 'coinbase': False,
- 'signatures': [None],
- }]
- tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
- if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
- fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
- sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val-fee)]
- sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
- sweep_tx.set_rbf(True)
- sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
- if not sweep_tx.is_complete():
- raise Exception('channel close sweep tx is not complete')
- return sweep_tx
-
-
-def create_sweeptx_ctx_to_local(address, ctx, output_idx: int, witness_script: str,
- privkey: bytes, is_revocation: bool,
- to_self_delay: int=None,
- fee_per_kb: int=None) -> Transaction:
- """Create a txn that sweeps the 'to_local' output of a commitment
- transaction into our wallet.
-
- privkey: either revocation_privkey or localdelayed_privkey
- is_revocation: tells us which ^
- """
- val = ctx.outputs()[output_idx].value
- sweep_inputs = [{
- 'scriptSig': '',
- 'type': 'p2wsh',
- 'signatures': [],
- 'num_sig': 0,
- 'prevout_n': output_idx,
- 'prevout_hash': ctx.txid(),
- 'value': val,
- 'coinbase': False,
- 'preimage_script': witness_script,
- }]
- if to_self_delay is not None:
- sweep_inputs[0]['sequence'] = to_self_delay
- tx_size_bytes = 121 # approx size of to_local -> p2wpkh
- if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
- fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
- sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val - fee)]
- sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
- sig = sweep_tx.sign_txin(0, privkey)
- witness = construct_witness([sig, int(is_revocation), witness_script])
- sweep_tx.inputs()[0]['witness'] = witness
- return sweep_tx
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -19,7 +19,7 @@ from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_ad
from .lnbase import Peer, aiosafe
from .lnaddr import lnencode, LnAddr, lndecode
from .ecc import der_sig_from_sig_string
-from .lnhtlc import HTLCStateMachine
+from .lnchan import Channel
from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
get_compressed_pubkey_from_bech32, extract_nodeid,
PaymentFailure, split_host_port, ConnStringFormatError,
@@ -50,7 +50,7 @@ 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.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))} # type: Dict[bytes, HTLCStateMachine]
+ self.channels = {x.channel_id: x for x in map(Channel, wallet.storage.get("channels", []))} # type: Dict[bytes, HTLCStateMachine]
for c in self.channels.values():
c.lnwatcher = network.lnwatcher
c.sweep_address = self.sweep_address
@@ -115,7 +115,7 @@ class LNWorker(PrintError):
return peer
def save_channel(self, openchannel):
- assert type(openchannel) is HTLCStateMachine
+ assert type(openchannel) is Channel
if openchannel.config[REMOTE].next_per_commitment_point == openchannel.config[REMOTE].current_per_commitment_point:
raise Exception("Tried to save channel with next_point == current_point, this should not happen")
with self.lock:
diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py
@@ -0,0 +1,367 @@
+# ported from lnd 42de4400bff5105352d0552155f73589166d162b
+
+import unittest
+import electrum.bitcoin as bitcoin
+import electrum.lnbase as lnbase
+import electrum.lnchan as lnchan
+import electrum.lnutil as lnutil
+import electrum.util as util
+import os
+import binascii
+
+from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
+
+def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv):
+ assert local_amount > 0
+ assert remote_amount > 0
+ channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
+ their_revocation_store = lnbase.RevocationStore()
+
+ return {
+ "channel_id":channel_id,
+ "short_channel_id":channel_id[:8],
+ "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index),
+ "remote_config":lnbase.RemoteConfig(
+ payment_basepoint=other_pubkeys[0],
+ multisig_key=other_pubkeys[1],
+ htlc_basepoint=other_pubkeys[2],
+ delayed_basepoint=other_pubkeys[3],
+ revocation_basepoint=other_pubkeys[4],
+ to_self_delay=r_csv,
+ dust_limit_sat=r_dust,
+ max_htlc_value_in_flight_msat=500000 * 1000,
+ max_accepted_htlcs=5,
+ initial_msat=remote_amount,
+ ctn = 0,
+ next_htlc_id = 0,
+ amount_msat=remote_amount,
+
+ next_per_commitment_point=nex,
+ current_per_commitment_point=cur,
+ revocation_store=their_revocation_store,
+ ),
+ "local_config":lnbase.LocalConfig(
+ payment_basepoint=privkeys[0],
+ multisig_key=privkeys[1],
+ htlc_basepoint=privkeys[2],
+ delayed_basepoint=privkeys[3],
+ revocation_basepoint=privkeys[4],
+ to_self_delay=l_csv,
+ dust_limit_sat=l_dust,
+ max_htlc_value_in_flight_msat=500000 * 1000,
+ max_accepted_htlcs=5,
+ initial_msat=local_amount,
+ ctn = 0,
+ next_htlc_id = 0,
+ amount_msat=local_amount,
+
+ per_commitment_secret_seed=seed,
+ funding_locked_received=True,
+ was_announced=False,
+ current_commitment_signature=None,
+ current_htlc_signatures=None,
+ ),
+ "constraints":lnbase.ChannelConstraints(
+ capacity=funding_sat,
+ is_initiator=is_initiator,
+ funding_txn_minimum_depth=3,
+ feerate=local_feerate,
+ ),
+ "node_id":other_node_id,
+ "remote_commitment_to_be_revoked": None,
+ 'onion_keys': {},
+ }
+
+def bip32(sequence):
+ xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard')
+ xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence)
+ xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv)
+ assert len(k) == 32
+ assert type(k) is bytes
+ return k
+
+def create_test_channels(feerate=6000, local=None, remote=None):
+ funding_txid = binascii.hexlify(os.urandom(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)
+ remote_amount = remote if remote is not None else (funding_sat * 1000 // 2)
+ alice_raw = [ bip32("m/" + str(i)) for i in range(5) ]
+ bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ]
+ alice_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw]
+ bob_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw]
+ 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_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"))
+
+ return \
+ 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), "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), "bob")
+
+one_bitcoin_in_msat = bitcoin.COIN * 1000
+
+class TestFee(unittest.TestCase):
+ """
+ test
+ https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2
+ """
+ def test_SimpleAddSettleWorkflow(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):
+ def assertOutputExistsByValue(self, tx, amt_sat):
+ for typ, scr, val in tx.outputs():
+ if val == amt_sat:
+ break
+ else:
+ self.assertFalse()
+
+ 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,
+ # and Bob having 5 BTC.
+ self.alice_channel, self.bob_channel = create_test_channels()
+
+ self.paymentPreimage = b"\x01" * 32
+ paymentHash = bitcoin.sha256(self.paymentPreimage)
+ self.htlc = {
+ 'payment_hash' : paymentHash,
+ 'amount_msat' : one_bitcoin_in_msat,
+ 'cltv_expiry' : 5,
+ }
+
+ # First Alice adds the outgoing HTLC to her local channel's state
+ # update log. Then Alice sends this wire message over to Bob who adds
+ # this htlc to his remote state update log.
+ self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc)
+
+ self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc)
+ self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0]
+
+ def test_SimpleAddSettleWorkflow(self):
+ alice_channel, bob_channel = self.alice_channel, self.bob_channel
+ htlc = self.htlc
+
+ # 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")
+
+ # 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.
+ bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)
+
+ # 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 finally send 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.
+ bobSig, bobHtlcSigs = bob_channel.sign_next_commitment()
+
+ # 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)
+
+ # 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)
+
+ # Alice then generates a revocation for bob.
+ aliceRevocation, _ = alice_channel.revoke_current_commitment()
+
+ # Finally Bob processes Alice's revocation, at this point the new HTLC
+ # 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.
+ bob_channel.receive_revocation(aliceRevocation)
+
+ # At this point, both sides should have the proper number of satoshis
+ # sent, and commitment height updated within their local channel
+ # state.
+ aliceSent = 0
+ bobSent = 0
+
+ self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent")
+ self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received")
+ self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent")
+ self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received")
+ self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height")
+ self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height")
+
+ # 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)
+
+ # 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)
+
+ alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex)
+
+ bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()
+ alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)
+
+ aliceRevocation2, _ = alice_channel.revoke_current_commitment()
+ aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment()
+ self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures")
+
+ received, sent = bob_channel.receive_revocation(aliceRevocation2)
+ self.assertEqual(received, one_bitcoin_in_msat)
+
+ bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)
+
+ bobRevocation2, _ = bob_channel.revoke_current_commitment()
+ alice_channel.receive_revocation(bobRevocation2)
+
+ # 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
+ # should show 1 BTC received. They should also be at commitment height
+ # two, with the revocation window extended by 1 (5).
+ mSatTransferred = one_bitcoin_in_msat
+ self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect")
+ self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect")
+ self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect")
+ self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect")
+ 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))
+ alice_channel.update_fee(100000)
+ alice_channel.serialize()
+
+ def alice_to_bob_fee_update(self):
+ fee = 111
+ self.alice_channel.update_fee(fee)
+ self.bob_channel.receive_update_fee(fee)
+ return fee
+
+ def test_UpdateFeeSenderCommits(self):
+ old_feerate = self.alice_channel.pending_feerate(LOCAL)
+ fee = self.alice_to_bob_fee_update()
+
+ alice_channel, bob_channel = self.alice_channel, self.bob_channel
+
+ self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate)
+ alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
+ self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate)
+
+ bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
+
+ self.assertNotEqual(fee, bob_channel.constraints.feerate)
+ rev, _ = bob_channel.revoke_current_commitment()
+ self.assertEqual(fee, bob_channel.constraints.feerate)
+
+ bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
+ alice_channel.receive_revocation(rev)
+ alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
+
+ self.assertNotEqual(fee, alice_channel.constraints.feerate)
+ rev, _ = alice_channel.revoke_current_commitment()
+ self.assertEqual(fee, alice_channel.constraints.feerate)
+
+ bob_channel.receive_revocation(rev)
+ self.assertEqual(fee, bob_channel.constraints.feerate)
+
+
+ def test_UpdateFeeReceiverCommits(self):
+ fee = self.alice_to_bob_fee_update()
+
+ alice_channel, bob_channel = self.alice_channel, self.bob_channel
+
+ bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
+ alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
+
+ alice_revocation, _ = alice_channel.revoke_current_commitment()
+ bob_channel.receive_revocation(alice_revocation)
+ alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
+ bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
+
+ self.assertNotEqual(fee, bob_channel.constraints.feerate)
+ bob_revocation, _ = bob_channel.revoke_current_commitment()
+ self.assertEqual(fee, bob_channel.constraints.feerate)
+
+ bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
+ alice_channel.receive_revocation(bob_revocation)
+ alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
+
+ self.assertNotEqual(fee, alice_channel.constraints.feerate)
+ alice_revocation, _ = alice_channel.revoke_current_commitment()
+ self.assertEqual(fee, alice_channel.constraints.feerate)
+
+ bob_channel.receive_revocation(alice_revocation)
+ self.assertEqual(fee, bob_channel.constraints.feerate)
+
+
+
+class TestDust(unittest.TestCase):
+ def test_DustLimit(self):
+ alice_channel, bob_channel = create_test_channels()
+
+ paymentPreimage = b"\x01" * 32
+ paymentHash = bitcoin.sha256(paymentPreimage)
+ fee_per_kw = alice_channel.constraints.feerate
+ self.assertEqual(fee_per_kw, 6000)
+ htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
+ self.assertEqual(htlcAmt, 4478)
+ htlc = {
+ 'payment_hash' : paymentHash,
+ 'amount_msat' : 1000 * htlcAmt,
+ 'cltv_expiry' : 5, # also in create_test_channels
+ }
+
+ 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)
+ 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(alice_channel.total_msat(SENT) // 1000, htlcAmt)
+
+def force_state_transition(chanA, chanB):
+ chanB.receive_new_commitment(*chanA.sign_next_commitment())
+ rev, _ = chanB.revoke_current_commitment()
+ bob_sig, bob_htlc_sigs = chanB.sign_next_commitment()
+ chanA.receive_revocation(rev)
+ chanA.receive_new_commitment(bob_sig, bob_htlc_sigs)
+ chanB.receive_revocation(chanA.revoke_current_commitment()[0])
+
+# calcStaticFee calculates appropriate fees for commitment transactions. This
+# function provides a simple way to allow test balance assertions to take fee
+# calculations into account.
+def calc_static_fee(numHTLCs):
+ commitWeight = 724
+ htlcWeight = 172
+ feePerKw = 24//4 * 1000
+ return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000
diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py
@@ -1,367 +0,0 @@
-# ported from lnd 42de4400bff5105352d0552155f73589166d162b
-
-import unittest
-import electrum.bitcoin as bitcoin
-import electrum.lnbase as lnbase
-import electrum.lnhtlc as lnhtlc
-import electrum.lnutil as lnutil
-import electrum.util as util
-import os
-import binascii
-
-from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
-
-def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv):
- assert local_amount > 0
- assert remote_amount > 0
- channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
- their_revocation_store = lnbase.RevocationStore()
-
- return {
- "channel_id":channel_id,
- "short_channel_id":channel_id[:8],
- "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index),
- "remote_config":lnbase.RemoteConfig(
- payment_basepoint=other_pubkeys[0],
- multisig_key=other_pubkeys[1],
- htlc_basepoint=other_pubkeys[2],
- delayed_basepoint=other_pubkeys[3],
- revocation_basepoint=other_pubkeys[4],
- to_self_delay=r_csv,
- dust_limit_sat=r_dust,
- max_htlc_value_in_flight_msat=500000 * 1000,
- max_accepted_htlcs=5,
- initial_msat=remote_amount,
- ctn = 0,
- next_htlc_id = 0,
- amount_msat=remote_amount,
-
- next_per_commitment_point=nex,
- current_per_commitment_point=cur,
- revocation_store=their_revocation_store,
- ),
- "local_config":lnbase.LocalConfig(
- payment_basepoint=privkeys[0],
- multisig_key=privkeys[1],
- htlc_basepoint=privkeys[2],
- delayed_basepoint=privkeys[3],
- revocation_basepoint=privkeys[4],
- to_self_delay=l_csv,
- dust_limit_sat=l_dust,
- max_htlc_value_in_flight_msat=500000 * 1000,
- max_accepted_htlcs=5,
- initial_msat=local_amount,
- ctn = 0,
- next_htlc_id = 0,
- amount_msat=local_amount,
-
- per_commitment_secret_seed=seed,
- funding_locked_received=True,
- was_announced=False,
- current_commitment_signature=None,
- current_htlc_signatures=None,
- ),
- "constraints":lnbase.ChannelConstraints(
- capacity=funding_sat,
- is_initiator=is_initiator,
- funding_txn_minimum_depth=3,
- feerate=local_feerate,
- ),
- "node_id":other_node_id,
- "remote_commitment_to_be_revoked": None,
- 'onion_keys': {},
- }
-
-def bip32(sequence):
- xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard')
- xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence)
- xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv)
- assert len(k) == 32
- assert type(k) is bytes
- return k
-
-def create_test_channels(feerate=6000, local=None, remote=None):
- funding_txid = binascii.hexlify(os.urandom(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)
- remote_amount = remote if remote is not None else (funding_sat * 1000 // 2)
- alice_raw = [ bip32("m/" + str(i)) for i in range(5) ]
- bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ]
- alice_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw]
- bob_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw]
- 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_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"))
-
- return \
- lnhtlc.HTLCStateMachine(
- 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), "alice"), \
- lnhtlc.HTLCStateMachine(
- 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), "bob")
-
-one_bitcoin_in_msat = bitcoin.COIN * 1000
-
-class TestFee(unittest.TestCase):
- """
- test
- https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2
- """
- def test_SimpleAddSettleWorkflow(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 TestLNBaseHTLCStateMachine(unittest.TestCase):
- def assertOutputExistsByValue(self, tx, amt_sat):
- for typ, scr, val in tx.outputs():
- if val == amt_sat:
- break
- else:
- self.assertFalse()
-
- 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,
- # and Bob having 5 BTC.
- self.alice_channel, self.bob_channel = create_test_channels()
-
- self.paymentPreimage = b"\x01" * 32
- paymentHash = bitcoin.sha256(self.paymentPreimage)
- self.htlc = {
- 'payment_hash' : paymentHash,
- 'amount_msat' : one_bitcoin_in_msat,
- 'cltv_expiry' : 5,
- }
-
- # First Alice adds the outgoing HTLC to her local channel's state
- # update log. Then Alice sends this wire message over to Bob who adds
- # this htlc to his remote state update log.
- self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc)
-
- self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc)
- self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0]
-
- def test_SimpleAddSettleWorkflow(self):
- alice_channel, bob_channel = self.alice_channel, self.bob_channel
- htlc = self.htlc
-
- # 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")
-
- # 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.
- bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)
-
- # 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 finally send 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.
- bobSig, bobHtlcSigs = bob_channel.sign_next_commitment()
-
- # 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)
-
- # 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)
-
- # Alice then generates a revocation for bob.
- aliceRevocation, _ = alice_channel.revoke_current_commitment()
-
- # Finally Bob processes Alice's revocation, at this point the new HTLC
- # 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.
- bob_channel.receive_revocation(aliceRevocation)
-
- # At this point, both sides should have the proper number of satoshis
- # sent, and commitment height updated within their local channel
- # state.
- aliceSent = 0
- bobSent = 0
-
- self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent")
- self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received")
- self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent")
- self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received")
- self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height")
- self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height")
-
- # 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)
-
- # 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)
-
- alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex)
-
- bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()
- alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)
-
- aliceRevocation2, _ = alice_channel.revoke_current_commitment()
- aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment()
- self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures")
-
- received, sent = bob_channel.receive_revocation(aliceRevocation2)
- self.assertEqual(received, one_bitcoin_in_msat)
-
- bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)
-
- bobRevocation2, _ = bob_channel.revoke_current_commitment()
- alice_channel.receive_revocation(bobRevocation2)
-
- # 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
- # should show 1 BTC received. They should also be at commitment height
- # two, with the revocation window extended by 1 (5).
- mSatTransferred = one_bitcoin_in_msat
- self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect")
- self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect")
- self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect")
- self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect")
- 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))
- alice_channel.update_fee(100000)
- alice_channel.serialize()
-
- def alice_to_bob_fee_update(self):
- fee = 111
- self.alice_channel.update_fee(fee)
- self.bob_channel.receive_update_fee(fee)
- return fee
-
- def test_UpdateFeeSenderCommits(self):
- old_feerate = self.alice_channel.pending_feerate(LOCAL)
- fee = self.alice_to_bob_fee_update()
-
- alice_channel, bob_channel = self.alice_channel, self.bob_channel
-
- self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate)
- alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
- self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate)
-
- bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
-
- self.assertNotEqual(fee, bob_channel.constraints.feerate)
- rev, _ = bob_channel.revoke_current_commitment()
- self.assertEqual(fee, bob_channel.constraints.feerate)
-
- bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
- alice_channel.receive_revocation(rev)
- alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
-
- self.assertNotEqual(fee, alice_channel.constraints.feerate)
- rev, _ = alice_channel.revoke_current_commitment()
- self.assertEqual(fee, alice_channel.constraints.feerate)
-
- bob_channel.receive_revocation(rev)
- self.assertEqual(fee, bob_channel.constraints.feerate)
-
-
- def test_UpdateFeeReceiverCommits(self):
- fee = self.alice_to_bob_fee_update()
-
- alice_channel, bob_channel = self.alice_channel, self.bob_channel
-
- bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
- alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
-
- alice_revocation, _ = alice_channel.revoke_current_commitment()
- bob_channel.receive_revocation(alice_revocation)
- alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
- bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
-
- self.assertNotEqual(fee, bob_channel.constraints.feerate)
- bob_revocation, _ = bob_channel.revoke_current_commitment()
- self.assertEqual(fee, bob_channel.constraints.feerate)
-
- bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
- alice_channel.receive_revocation(bob_revocation)
- alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
-
- self.assertNotEqual(fee, alice_channel.constraints.feerate)
- alice_revocation, _ = alice_channel.revoke_current_commitment()
- self.assertEqual(fee, alice_channel.constraints.feerate)
-
- bob_channel.receive_revocation(alice_revocation)
- self.assertEqual(fee, bob_channel.constraints.feerate)
-
-
-
-class TestLNHTLCDust(unittest.TestCase):
- def test_HTLCDustLimit(self):
- alice_channel, bob_channel = create_test_channels()
-
- paymentPreimage = b"\x01" * 32
- paymentHash = bitcoin.sha256(paymentPreimage)
- fee_per_kw = alice_channel.constraints.feerate
- self.assertEqual(fee_per_kw, 6000)
- htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
- self.assertEqual(htlcAmt, 4478)
- htlc = {
- 'payment_hash' : paymentHash,
- 'amount_msat' : 1000 * htlcAmt,
- 'cltv_expiry' : 5, # also in create_test_channels
- }
-
- 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)
- 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(alice_channel.total_msat(SENT) // 1000, htlcAmt)
-
-def force_state_transition(chanA, chanB):
- chanB.receive_new_commitment(*chanA.sign_next_commitment())
- rev, _ = chanB.revoke_current_commitment()
- bob_sig, bob_htlc_sigs = chanB.sign_next_commitment()
- chanA.receive_revocation(rev)
- chanA.receive_new_commitment(bob_sig, bob_htlc_sigs)
- chanB.receive_revocation(chanA.revoke_current_commitment()[0])
-
-# calcStaticFee calculates appropriate fees for commitment transactions. This
-# function provides a simple way to allow test balance assertions to take fee
-# calculations into account.
-def calc_static_fee(numHTLCs):
- commitWeight = 724
- htlcWeight = 172
- feePerKw = 24//4 * 1000
- return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000
diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
@@ -7,7 +7,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
ScriptHtlc, extract_nodeid)
-from electrum import lnhtlc
+from electrum import lnchan
from electrum.util import bh2u, bfh
from electrum.transaction import Transaction
@@ -496,7 +496,7 @@ class TestLNUtil(unittest.TestCase):
(1, 2000 * 1000),
(3, 3000 * 1000),
(4, 4000 * 1000)]:
- htlc_obj[num] = lnhtlc.UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=None, htlc_id=None)
+ htlc_obj[num] = lnchan.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(