Browse Source

lnhtlc: save logs and feeupdates

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
d5d9270d0c
  1. 5
      electrum/gui/qt/channels_list.py
  2. 25
      electrum/lnbase.py
  3. 138
      electrum/lnhtlc.py
  4. 2
      electrum/lnutil.py
  5. 31
      electrum/tests/test_lnhtlc.py

5
electrum/gui/qt/channels_list.py

@ -6,6 +6,7 @@ from electrum.util import inv_dict, bh2u, bfh
from electrum.i18n import _
from electrum.lnhtlc import HTLCStateMachine
from electrum.lnaddr import lndecode
from electrum.lnutil import LOCAL, REMOTE
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
from .amountedit import BTCAmountEdit
@ -24,8 +25,8 @@ class ChannelsList(MyTreeWidget):
def format_fields(self, chan):
return [
bh2u(chan.node_id),
self.parent.format_amount(chan.local_state.amount_msat//1000),
self.parent.format_amount(chan.remote_state.amount_msat//1000),
self.parent.format_amount(chan.balance(LOCAL)//1000),
self.parent.format_amount(chan.balance(REMOTE)//1000),
chan.get_state()
]

25
electrum/lnbase.py

@ -8,6 +8,7 @@ from collections import namedtuple, defaultdict, OrderedDict, defaultdict
from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
from .lnutil import sign_and_get_sig_string, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed
from .lnutil import secret_to_pubkey, LNPeerAddr, PaymentFailure
from .lnutil import LOCAL, REMOTE
from .bitcoin import COIN
from ecdsa.util import sigdecode_der, sigencode_string_canonize, sigdecode_string
@ -33,7 +34,7 @@ from .util import PrintError, bh2u, print_error, bfh, aiosafe
from .transaction import opcodes, Transaction, TxOutput
from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, ONION_FAILURE_CODE_MAP
from .lnaddr import lndecode
from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc
from .lnhtlc import HTLCStateMachine, RevokeAndAck
def channel_id_from_funding_tx(funding_txid, funding_index):
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
@ -496,7 +497,8 @@ class Peer(PrintError):
to_self_delay=143,
dust_limit_sat=546,
max_htlc_value_in_flight_msat=0xffffffffffffffff,
max_accepted_htlcs=5
max_accepted_htlcs=5,
initial_msat=funding_sat * 1000 - push_msat,
)
# TODO derive this?
per_commitment_secret_seed = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big')
@ -536,7 +538,8 @@ class Peer(PrintError):
to_self_delay=int.from_bytes(payload['to_self_delay'], byteorder='big'),
dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], byteorder='big'),
max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'),
max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big')
max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'),
initial_msat=push_msat
)
funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
assert remote_config.dust_limit_sat < 600
@ -844,9 +847,9 @@ class Peer(PrintError):
onion = new_onion_packet([x.node_id for x in route], self.secret_key, hops_data, associated_data)
amount_msat += total_fee
# FIXME this below will probably break with multiple HTLCs
msat_local = chan.local_state.amount_msat - amount_msat
msat_remote = chan.remote_state.amount_msat + amount_msat
htlc = UpdateAddHtlc(amount_msat, payment_hash, final_cltv_expiry_with_deltas)
msat_local = chan.balance(LOCAL) - amount_msat
msat_remote = chan.balance(REMOTE) + amount_msat
htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':final_cltv_expiry_with_deltas}
# FIXME if we raise here, this channel will not get blacklisted, and the payment can never succeed,
# as we will just keep retrying this same path. using the current blacklisting is not a solution as
@ -861,10 +864,10 @@ class Peer(PrintError):
# FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test.
raise PaymentFailure('not enough local balance')
self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=chan.local_state.next_htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes()))
htlc_id = chan.add_htlc(htlc)
self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes()))
chan.add_htlc(htlc)
self.attempted_route[(chan.channel_id, htlc.htlc_id)] = route
self.attempted_route[(chan.channel_id, htlc_id)] = route
sig_64, htlc_sigs = chan.sign_next_commitment()
self.send_message(gen_msg("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs)))
@ -947,7 +950,7 @@ class Peer(PrintError):
assert amount_msat == expected_received_msat
payment_hash = htlc["payment_hash"]
htlc = UpdateAddHtlc(amount_msat, payment_hash, cltv_expiry)
htlc = {'amount_msat': amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry}
chan.receive_htlc(htlc)
@ -967,7 +970,7 @@ class Peer(PrintError):
# remote commitment transaction without htlcs
# FIXME why is this not using the HTLC state machine?
bare_ctx = chan.make_commitment(chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point,
chan.remote_state.amount_msat - expected_received_msat, chan.local_state.amount_msat + expected_received_msat)
chan.balance(REMOTE) - expected_received_msat, chan.balance(LOCAL) + expected_received_msat)
self.lnwatcher.process_new_offchain_ctx(chan, bare_ctx, ours=False)
sig_64 = sign_and_get_sig_string(bare_ctx, chan.local_config, chan.remote_config)
self.send_message(gen_msg("commitment_signed", channel_id=channel_id, signature=sig_64, num_htlcs=0))

138
electrum/lnhtlc.py

@ -16,7 +16,7 @@ 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, extract_ctn_from_tx_and_chan
from .lnutil import LOCAL, REMOTE, SENT, RECEIVED
from .lnutil import LOCAL, REMOTE, SENT, RECEIVED, HTLCOwner
from .transaction import Transaction
@ -34,12 +34,22 @@ FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED
FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED
COMMITTED = FeeUpdateProgress.COMMITTED
class FeeUpdate:
from collections import namedtuple
def __init__(self, chan, feerate):
self.rate = feerate
self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn
self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None}
class FeeUpdate:
def __init__(self, chan, **kwargs):
if 'rate' in kwargs:
self.rate = kwargs['rate']
else:
assert False
if 'proposed' not in kwargs:
self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn
else:
self.proposed = kwargs['proposed']
if 'progress' not in kwargs:
self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None}
else:
self.progress = {FeeUpdateProgress[x.partition('.')[2]]: y for x,y in kwargs['progress'].items()}
self.chan = chan
@property
@ -65,30 +75,30 @@ class FeeUpdate:
if subject == LOCAL and not self.chan.constraints.is_initiator:
return self.rate
class UpdateAddHtlc:
def __init__(self, amount_msat, payment_hash, cltv_expiry):
self.amount_msat = amount_msat
self.payment_hash = payment_hash
self.cltv_expiry = cltv_expiry
# the height the htlc was locked in at, or None
self.locked_in = {LOCAL: None, REMOTE: None}
self.settled = {LOCAL: None, REMOTE: None}
self.htlc_id = None
def as_tuple(self):
return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.locked_in[REMOTE], self.locked_in[LOCAL], self.settled)
def __hash__(self):
return hash(self.as_tuple())
def __eq__(self, o):
return type(o) is UpdateAddHtlc and self.as_tuple() == o.as_tuple()
def __repr__(self):
return "UpdateAddHtlc" + str(self.as_tuple())
def to_save(self):
return {'rate': self.rate, 'proposed': self.proposed, 'progress': self.progress}
class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'settled', '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()}
args[4] = {HTLCOwner(int(x)): y for x,y in args[4].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']}
if 'settled' not in kwargs:
kwargs['settled'] = {LOCAL: None, REMOTE: None}
else:
kwargs['settled'] = {HTLCOwner(int(x)): y for x,y in kwargs['settled']}
return super().__new__(cls, **kwargs)
is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
@ -155,10 +165,22 @@ class HTLCStateMachine(PrintError):
self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"])
self.log = {LOCAL: [], REMOTE: []}
for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
if strname not in state: continue
for typ,y in state[strname]:
if typ == "UpdateAddHtlc":
self.log[subject].append(UpdateAddHtlc(*decodeAll(y)))
elif typ == "SettleHtlc":
self.log[subject].append(SettleHtlc(*decodeAll(y)))
else:
assert False
self.name = name
self.fee_mgr = []
if 'fee_updates' in state:
for y in state['fee_updates']:
self.fee_mgr.append(FeeUpdate(self, **y))
self.local_commitment = self.pending_local_commitment
self.remote_commitment = self.pending_remote_commitment
@ -190,13 +212,12 @@ class HTLCStateMachine(PrintError):
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 UpdateAddHtlc
assert type(htlc) is dict
htlc = UpdateAddHtlc(**htlc, htlc_id=self.local_state.next_htlc_id)
self.log[LOCAL].append(htlc)
self.print_error("add_htlc")
htlc_id = self.local_state.next_htlc_id
self.local_state=self.local_state._replace(next_htlc_id=htlc_id + 1)
htlc.htlc_id = htlc_id
return htlc_id
self.local_state=self.local_state._replace(next_htlc_id=htlc.htlc_id + 1)
return htlc.htlc_id
def receive_htlc(self, htlc):
"""
@ -204,13 +225,12 @@ class HTLCStateMachine(PrintError):
method should be called in response to receiving a new HTLC from the remote
party.
"""
self.print_error("receive_htlc")
assert type(htlc) is UpdateAddHtlc
assert type(htlc) is dict
htlc = UpdateAddHtlc(**htlc, htlc_id = self.remote_state.next_htlc_id)
self.log[REMOTE].append(htlc)
htlc_id = self.remote_state.next_htlc_id
self.remote_state=self.remote_state._replace(next_htlc_id=htlc_id + 1)
htlc.htlc_id = htlc_id
return htlc_id
self.print_error("receive_htlc")
self.remote_state=self.remote_state._replace(next_htlc_id=htlc.htlc_id + 1)
return htlc.htlc_id
def sign_next_commitment(self):
"""
@ -431,6 +451,9 @@ class HTLCStateMachine(PrintError):
amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch)
)
self.balance(LOCAL)
self.balance(REMOTE)
for pending_fee in self.fee_mgr:
if pending_fee.is_proposed():
if self.constraints.is_initiator:
@ -441,6 +464,26 @@ class HTLCStateMachine(PrintError):
self.remote_commitment_to_be_revoked = prev_remote_commitment
return received_this_batch, sent_this_batch
def balance(self, subject):
initial = self.local_config.initial_msat if subject == LOCAL else self.remote_config.initial_msat
for x in self.log[-subject]:
if type(x) is not SettleHtlc: continue
htlc = self.lookup_htlc(self.log[subject], x.htlc_id)
htlc_height = htlc.settled[subject]
if htlc_height is not None and htlc_height <= self.current_height[subject]:
initial -= htlc.amount_msat
for x in self.log[subject]:
if type(x) is not SettleHtlc: continue
htlc = self.lookup_htlc(self.log[-subject], x.htlc_id)
htlc_height = htlc.settled[-subject]
if htlc_height is not None and htlc_height <= self.current_height[-subject]:
initial += htlc.amount_msat
assert initial == (self.local_state.amount_msat if subject == LOCAL else self.remote_state.amount_msat)
return initial
@staticmethod
def htlcsum(htlcs):
amount_unsettled = 0
@ -611,13 +654,13 @@ class HTLCStateMachine(PrintError):
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, feerate)
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, feerate)
pending_fee = FeeUpdate(self, rate=feerate)
self.fee_mgr.append(pending_fee)
def to_save(self):
@ -632,6 +675,9 @@ class HTLCStateMachine(PrintError):
"funding_outpoint": self.funding_outpoint,
"node_id": self.node_id,
"remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked),
"remote_log": [(type(x).__name__, x) for x in self.log[REMOTE]],
"local_log": [(type(x).__name__, x) for x in self.log[LOCAL]],
"fee_updates": [x.to_save() for x in self.fee_mgr],
}
def serialize(self):
@ -643,7 +689,13 @@ class HTLCStateMachine(PrintError):
return binascii.hexlify(o).decode("ascii")
if isinstance(o, RevocationStore):
return o.serialize()
if isinstance(o, SettleHtlc):
return json.dumps(('SettleHtlc', namedtuples_to_dict(o)))
if isinstance(o, UpdateAddHtlc):
return json.dumps(('UpdateAddHtlc', namedtuples_to_dict(o)))
return super(MyJsonEncoder, self)
for fee_upd in serialized_channel['fee_updates']:
fee_upd['progress'] = {str(k): v for k,v in fee_upd['progress'].items()}
dumped = MyJsonEncoder().encode(serialized_channel)
roundtripped = json.loads(dumped)
reconstructed = HTLCStateMachine(roundtripped)

2
electrum/lnutil.py

@ -18,7 +18,7 @@ HTLC_SUCCESS_WEIGHT = 703
Keypair = namedtuple("Keypair", ["pubkey", "privkey"])
ChannelConfig = namedtuple("ChannelConfig", [
"payment_basepoint", "multisig_key", "htlc_basepoint", "delayed_basepoint", "revocation_basepoint",
"to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs"])
"to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs", "initial_msat"])
OnlyPubkeyKeypair = namedtuple("OnlyPubkeyKeypair", ["pubkey"])
RemoteState = namedtuple("RemoteState", ["ctn", "next_per_commitment_point", "amount_msat", "revocation_store", "current_per_commitment_point", "next_htlc_id", "feerate"])
LocalState = namedtuple("LocalState", ["ctn", "per_commitment_secret_seed", "amount_msat", "next_htlc_id", "funding_locked_received", "was_announced", "current_commitment_signature", "current_htlc_signatures", "feerate"])

31
electrum/tests/test_lnhtlc.py

@ -25,7 +25,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
to_self_delay=l_csv,
dust_limit_sat=l_dust,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
max_accepted_htlcs=5,
initial_msat=local_amount,
)
remote_config=lnbase.ChannelConfig(
payment_basepoint=other_pubkeys[0],
@ -36,7 +37,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
to_self_delay=r_csv,
dust_limit_sat=r_dust,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
max_accepted_htlcs=5,
initial_msat=remote_amount,
)
return {
@ -132,11 +134,11 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage)
self.htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = one_bitcoin_in_msat,
cltv_expiry = 5,
)
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
@ -144,6 +146,7 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
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][0]
def test_SimpleAddSettleWorkflow(self):
alice_channel, bob_channel = self.alice_channel, self.bob_channel
@ -250,6 +253,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
# 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
@ -325,11 +330,11 @@ class TestLNHTLCDust(unittest.TestCase):
self.assertEqual(fee_per_kw, 6000)
htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
self.assertEqual(htlcAmt, 4478)
htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = 1000 * htlcAmt,
cltv_expiry = 5, # also in create_test_channels
)
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)
@ -338,7 +343,7 @@ class TestLNHTLCDust(unittest.TestCase):
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, htlc.htlc_id)
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)

Loading…
Cancel
Save