commit d5d9270d0c3d09972ec94fa90c016449a93077ea
parent eca5545004bbe07ba7ead674bedff290b4664f33
Author: Janus <ysangkok@gmail.com>
Date: Tue, 18 Sep 2018 18:38:57 +0200
lnhtlc: save logs and feeupdates
Diffstat:
5 files changed, 131 insertions(+), 70 deletions(-)
diff --git a/electrum/gui/qt/channels_list.py b/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()
]
diff --git a/electrum/lnbase.py b/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))
diff --git a/electrum/lnhtlc.py b/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)
diff --git a/electrum/lnutil.py b/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"])
diff --git a/electrum/tests/test_lnhtlc.py b/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)