Browse Source

lnhtlc: merge config and state, remove unnecessary properties

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
e8471e483b
  1. 188
      electrum/lnbase.py
  2. 258
      electrum/lnhtlc.py
  3. 48
      electrum/lnutil.py
  4. 12
      electrum/lnworker.py
  5. 90
      electrum/tests/test_lnhtlc.py

188
electrum/lnbase.py

@ -27,9 +27,9 @@ 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
from .lnutil import (Outpoint, ChannelConfig, LocalState,
RemoteState, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
from .lnhtlc import HTLCStateMachine, RevokeAndAck, htlcsum
from .lnutil import (Outpoint, LocalConfig, ChannelConfig,
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
funding_output_script, get_ecdh, get_per_commitment_secret_from_seed,
secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures,
LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily,
@ -480,7 +480,7 @@ class Peer(PrintError):
def on_announcement_signatures(self, payload):
channel_id = payload['channel_id']
chan = self.channels[payload['channel_id']]
if chan.local_state.was_announced:
if chan.config[LOCAL].was_announced:
h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan)
else:
self.announcement_signatures[channel_id].put_nowait(payload)
@ -530,7 +530,7 @@ class Peer(PrintError):
chan.set_state('DISCONNECTED')
self.network.trigger_callback('channel', chan)
def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner):
def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner, feerate):
# key derivation
channel_counter = self.lnworker.get_and_inc_counter_for_channel_keys()
keypair_generator = lambda family: generate_keypair(self.lnworker.ln_keystore, family, channel_counter)
@ -549,6 +549,10 @@ class Peer(PrintError):
max_htlc_value_in_flight_msat=0xffffffffffffffff,
max_accepted_htlcs=5,
initial_msat=initial_msat,
ctn=-1,
next_htlc_id=0,
amount_msat=initial_msat,
feerate=feerate,
)
per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
return local_config, per_commitment_secret_seed
@ -556,9 +560,8 @@ class Peer(PrintError):
@log_exceptions
async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id):
await self.initialized
local_config, per_commitment_secret_seed = self.make_local_config(funding_sat, push_msat, LOCAL)
# amounts
local_feerate = self.current_feerate_per_kw()
feerate = self.current_feerate_per_kw()
local_config, per_commitment_secret_seed = self.make_local_config(funding_sat, push_msat, LOCAL, feerate)
# for the first commitment transaction
per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX)
per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
@ -569,7 +572,7 @@ class Peer(PrintError):
funding_satoshis=funding_sat,
push_msat=push_msat,
dust_limit_satoshis=local_config.dust_limit_sat,
feerate_per_kw=local_feerate,
feerate_per_kw=feerate,
max_accepted_htlcs=local_config.max_accepted_htlcs,
funding_pubkey=local_config.multisig_key.pubkey,
revocation_basepoint=local_config.revocation_basepoint.pubkey,
@ -587,24 +590,33 @@ class Peer(PrintError):
if payload.get('error'):
raise Exception(payload.get('error'))
remote_per_commitment_point = payload['first_per_commitment_point']
remote_config=ChannelConfig(
funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big')
assert remote_dust_limit_sat < 600, remote_dust_limit_sat
assert int.from_bytes(payload['htlc_minimum_msat'], 'big') < 600 * 1000
remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big')
assert remote_max >= 198 * 1000 * 1000, remote_max
their_revocation_store = RevocationStore()
remote_config = RemoteConfig(
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),
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'),
dust_limit_sat=remote_dust_limit_sat,
max_htlc_value_in_flight_msat=remote_max,
max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'),
initial_msat=push_msat
initial_msat=push_msat,
ctn = -1,
amount_msat=push_msat,
next_htlc_id = 0,
feerate=feerate,
next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None,
revocation_store=their_revocation_store,
)
funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
assert remote_config.dust_limit_sat < 600
assert int.from_bytes(payload['htlc_minimum_msat'], 'big') < 600 * 1000
assert remote_config.max_htlc_value_in_flight_msat >= 198 * 1000 * 1000, remote_config.max_htlc_value_in_flight_msat
self.print_error('remote delay', remote_config.to_self_delay)
self.print_error('funding_txn_minimum_depth', funding_txn_minimum_depth)
# create funding tx
redeem_script = funding_output_script(local_config, remote_config)
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
@ -612,38 +624,21 @@ class Peer(PrintError):
funding_tx = self.lnworker.wallet.mktx([funding_output], password, self.lnworker.config, 1000)
funding_txid = funding_tx.txid()
funding_index = funding_tx.outputs().index(funding_output)
# compute amounts
local_amount = funding_sat*1000 - push_msat
remote_amount = push_msat
# remote commitment transaction
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index)
their_revocation_store = RevocationStore()
chan = {
"node_id": self.pubkey,
"channel_id": channel_id,
"short_channel_id": None,
"funding_outpoint": Outpoint(funding_txid, funding_index),
"local_config": local_config,
"remote_config": remote_config,
"remote_state": RemoteState(
ctn = -1,
next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None,
amount_msat=remote_amount,
revocation_store=their_revocation_store,
next_htlc_id = 0,
feerate=local_feerate
),
"local_state": LocalState(
ctn = -1,
"local_config": LocalConfig(
**local_config._asdict(),
per_commitment_secret_seed=per_commitment_secret_seed,
amount_msat=local_amount,
next_htlc_id = 0,
funding_locked_received = False,
was_announced = False,
current_commitment_signature = None,
current_htlc_signatures = None,
feerate=local_feerate
),
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
"remote_commitment_to_be_revoked": None,
@ -665,8 +660,8 @@ class Peer(PrintError):
success, _txid = await self.network.broadcast_transaction(funding_tx)
assert success, success
m.remote_commitment_to_be_revoked = m.pending_remote_commitment
m.remote_state = m.remote_state._replace(ctn=0)
m.local_state = m.local_state._replace(ctn=0, current_commitment_signature=remote_sig)
m.config[REMOTE] = m.config[REMOTE]._replace(ctn=0)
m.config[LOCAL] = m.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig)
m.set_state('OPENING')
return m
@ -677,21 +672,10 @@ class Peer(PrintError):
raise Exception('wrong chain_hash')
funding_sat = int.from_bytes(payload['funding_satoshis'], 'big')
push_msat = int.from_bytes(payload['push_msat'], 'big')
feerate = int.from_bytes(payload['feerate_per_kw'], 'big')
remote_config = ChannelConfig(
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']),
htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),
to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'),
dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], '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'),
initial_msat=funding_sat * 1000 - push_msat,
)
temp_chan_id = payload['temporary_channel_id']
local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE)
local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE, feerate)
# for the first commitment transaction
per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX)
@ -719,33 +703,39 @@ class Peer(PrintError):
funding_txid = bh2u(funding_created['funding_txid'][::-1])
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
their_revocation_store = RevocationStore()
local_feerate = int.from_bytes(payload['feerate_per_kw'], 'big')
remote_balance_sat = funding_sat * 1000 - push_msat
chan = {
"node_id": self.pubkey,
"channel_id": channel_id,
"short_channel_id": None,
"funding_outpoint": Outpoint(funding_txid, funding_idx),
"local_config": local_config,
"remote_config": remote_config,
"remote_state": RemoteState(
"remote_config": RemoteConfig(
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']),
htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),
to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'),
dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], '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'),
initial_msat=remote_balance_sat,
ctn = -1,
amount_msat=remote_balance_sat,
next_htlc_id = 0,
feerate=feerate,
next_per_commitment_point=payload['first_per_commitment_point'],
current_per_commitment_point=None,
amount_msat=remote_config.initial_msat,
revocation_store=their_revocation_store,
next_htlc_id = 0,
feerate=local_feerate
),
"local_state": LocalState(
ctn = -1,
"local_config": LocalConfig(
**local_config._asdict(),
per_commitment_secret_seed=per_commitment_secret_seed,
amount_msat=local_config.initial_msat,
next_htlc_id = 0,
funding_locked_received = False,
was_announced = False,
current_commitment_signature = None,
current_htlc_signatures = None,
feerate=local_feerate
),
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth),
"remote_commitment_to_be_revoked": None,
@ -762,8 +752,8 @@ class Peer(PrintError):
))
m.set_state('OPENING')
m.remote_commitment_to_be_revoked = m.pending_remote_commitment
m.remote_state = m.remote_state._replace(ctn=0)
m.local_state = m.local_state._replace(ctn=0, current_commitment_signature=remote_sig)
m.config[REMOTE] = m.config[REMOTE]._replace(ctn=0)
m.config[LOCAL] = m.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig)
self.lnworker.save_channel(m)
self.lnwatcher.watch_channel(m.get_funding_address(), m.funding_outpoint.to_str())
self.lnworker.on_channels_updated()
@ -776,7 +766,7 @@ class Peer(PrintError):
else:
break
outp = funding_tx.outputs()[funding_idx]
redeem_script = funding_output_script(remote_config, local_config)
redeem_script = funding_output_script(m.config[REMOTE], m.config[LOCAL])
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat):
m.set_state('DISCONNECTED')
@ -794,12 +784,12 @@ class Peer(PrintError):
self.network.trigger_callback('channel', chan)
self.send_message(gen_msg("channel_reestablish",
channel_id=chan_id,
next_local_commitment_number=chan.local_state.ctn+1,
next_remote_revocation_number=chan.remote_state.ctn
next_local_commitment_number=chan.config[LOCAL].ctn+1,
next_remote_revocation_number=chan.config[REMOTE].ctn
))
await self.channel_reestablished[chan_id]
chan.set_state('OPENING')
if chan.local_state.funding_locked_received and chan.short_channel_id:
if chan.config[LOCAL].funding_locked_received and chan.short_channel_id:
self.mark_open(chan)
self.network.trigger_callback('channel', chan)
@ -822,24 +812,24 @@ class Peer(PrintError):
channel_reestablish_msg = payload
# compare remote ctns
remote_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big')
if remote_ctn != chan.remote_state.ctn + 1:
self.print_error("expected remote ctn {}, got {}".format(chan.remote_state.ctn + 1, remote_ctn))
if remote_ctn != chan.config[REMOTE].ctn + 1:
self.print_error("expected remote ctn {}, got {}".format(chan.config[REMOTE].ctn + 1, remote_ctn))
# TODO iff their ctn is lower than ours, we should force close instead
try_to_get_remote_to_force_close_with_their_latest()
return
# compare local ctns
local_ctn = int.from_bytes(channel_reestablish_msg["next_remote_revocation_number"], 'big')
if local_ctn != chan.local_state.ctn:
self.print_error("expected local ctn {}, got {}".format(chan.local_state.ctn, local_ctn))
if local_ctn != chan.config[LOCAL].ctn:
self.print_error("expected local ctn {}, got {}".format(chan.config[LOCAL].ctn, local_ctn))
# TODO iff their ctn is lower than ours, we should force close instead
try_to_get_remote_to_force_close_with_their_latest()
return
# compare per commitment points (needs data_protect option)
their_pcp = channel_reestablish_msg.get("my_current_per_commitment_point", None)
if their_pcp is not None:
our_pcp = chan.remote_state.current_per_commitment_point
our_pcp = chan.config[REMOTE].current_per_commitment_point
if our_pcp is None:
our_pcp = chan.remote_state.next_per_commitment_point
our_pcp = chan.config[REMOTE].next_per_commitment_point
if our_pcp != their_pcp:
self.print_error("Remote PCP mismatch: {} {}".format(bh2u(our_pcp), bh2u(their_pcp)))
# FIXME ...what now?
@ -852,10 +842,10 @@ class Peer(PrintError):
channel_id = chan.channel_id
per_commitment_secret_index = RevocationStore.START_INDEX - 1
per_commitment_point_second = secret_to_pubkey(int.from_bytes(
get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, per_commitment_secret_index), 'big'))
get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big'))
# note: if funding_locked was not yet received, we might send it multiple times
self.send_message(gen_msg("funding_locked", channel_id=channel_id, next_per_commitment_point=per_commitment_point_second))
if chan.local_state.funding_locked_received:
if chan.config[LOCAL].funding_locked_received:
self.mark_open(chan)
def on_funding_locked(self, payload):
@ -864,13 +854,13 @@ class Peer(PrintError):
if not chan:
print(self.channels)
raise Exception("Got unknown funding_locked", channel_id)
if not chan.local_state.funding_locked_received:
our_next_point = chan.remote_state.next_per_commitment_point
if not chan.config[LOCAL].funding_locked_received:
our_next_point = chan.config[REMOTE].next_per_commitment_point
their_next_point = payload["next_per_commitment_point"]
new_remote_state = chan.remote_state._replace(next_per_commitment_point=their_next_point, current_per_commitment_point=our_next_point)
new_local_state = chan.local_state._replace(funding_locked_received = True)
chan.remote_state=new_remote_state
chan.local_state=new_local_state
new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point, current_per_commitment_point=our_next_point)
new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True)
chan.config[REMOTE]=new_remote_state
chan.config[LOCAL]=new_local_state
self.lnworker.save_channel(chan)
if chan.short_channel_id:
self.mark_open(chan)
@ -881,11 +871,11 @@ class Peer(PrintError):
Runs on the Network thread.
"""
if not chan.local_state.was_announced and funding_tx_depth >= 6:
if not chan.config[LOCAL].was_announced and funding_tx_depth >= 6:
# don't announce our channels
# FIXME should this be a field in chan.local_state maybe?
return
chan.local_state=chan.local_state._replace(was_announced=True)
chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True)
coro = self.handle_announcements(chan)
self.lnworker.save_channel(chan)
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
@ -896,7 +886,7 @@ class Peer(PrintError):
announcement_signatures_msg = await self.announcement_signatures[chan.channel_id].get()
remote_node_sig = announcement_signatures_msg["node_signature"]
remote_bitcoin_sig = announcement_signatures_msg["bitcoin_signature"]
if not ecc.verify_signature(chan.remote_config.multisig_key.pubkey, remote_bitcoin_sig, h):
if not ecc.verify_signature(chan.config[REMOTE].multisig_key.pubkey, remote_bitcoin_sig, h):
raise Exception("bitcoin_sig invalid in announcement_signatures")
if not ecc.verify_signature(self.pubkey, remote_node_sig, h):
raise Exception("node_sig invalid in announcement_signatures")
@ -904,7 +894,7 @@ class Peer(PrintError):
node_sigs = [local_node_sig, remote_node_sig]
bitcoin_sigs = [local_bitcoin_sig, remote_bitcoin_sig]
node_ids = [privkey_to_pubkey(self.privkey), self.pubkey]
bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey]
if node_ids[0] > node_ids[1]:
node_sigs.reverse()
@ -935,14 +925,14 @@ class Peer(PrintError):
if chan.get_state() == "OPEN":
return
# NOTE: even closed channels will be temporarily marked "OPEN"
assert chan.local_state.funding_locked_received
assert chan.config[LOCAL].funding_locked_received
chan.set_state("OPEN")
self.network.trigger_callback('channel', chan)
# add channel to database
pubkey_ours = self.lnworker.node_keypair.pubkey
pubkey_theirs = self.pubkey
node_ids = [pubkey_theirs, pubkey_ours]
bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey]
sorted_node_ids = list(sorted(node_ids))
if sorted_node_ids != node_ids:
node_ids = sorted_node_ids
@ -977,8 +967,8 @@ class Peer(PrintError):
def send_announcement_signatures(self, chan):
bitcoin_keys = [chan.local_config.multisig_key.pubkey,
chan.remote_config.multisig_key.pubkey]
bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey,
chan.config[REMOTE].multisig_key.pubkey]
node_ids = [privkey_to_pubkey(self.privkey),
self.pubkey]
@ -1000,7 +990,7 @@ class Peer(PrintError):
)
to_hash = chan_ann[256+2:]
h = bitcoin.Hash(to_hash)
bitcoin_signature = ecc.ECPrivkey(chan.local_config.multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string)
bitcoin_signature = ecc.ECPrivkey(chan.config[LOCAL].multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string)
node_signature = ecc.ECPrivkey(self.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string)
self.send_message(gen_msg("announcement_signatures",
channel_id=chan.channel_id,
@ -1105,10 +1095,10 @@ class Peer(PrintError):
# then no other payment can use this channel either.
# we need finer blacklisting -- e.g. a blacklist for just this "payment session"?
# or blacklist entries could store an msat value and also expire
if len(chan.htlcs_in_local) + 1 > chan.remote_config.max_accepted_htlcs:
if len(chan.htlcs(LOCAL, only_pending=True)) + 1 > chan.config[REMOTE].max_accepted_htlcs:
raise PaymentFailure('too many HTLCs already in channel')
if chan.htlcsum(chan.htlcs_in_local) + amount_msat > chan.remote_config.max_htlc_value_in_flight_msat:
raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(chan.remote_config.max_htlc_value_in_flight_msat))
if htlcsum(chan.htlcs(LOCAL, only_pending=True)) + amount_msat > chan.config[REMOTE].max_htlc_value_in_flight_msat:
raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(chan.config[REMOTE].max_htlc_value_in_flight_msat))
if msat_local < 0:
# FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test.
raise PaymentFailure('not enough local balance')
@ -1144,7 +1134,7 @@ class Peer(PrintError):
channel_id = chan.channel_id
expected_received_msat = int(decoded.amount * bitcoin.COIN * 1000)
htlc_id = int.from_bytes(htlc["id"], 'big')
assert htlc_id == chan.remote_state.next_htlc_id, (htlc_id, chan.remote_state.next_htlc_id)
assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id)
assert chan.get_state() == "OPEN"
cltv_expiry = int.from_bytes(htlc["cltv_expiry"], 'big')
# TODO verify sanity of their cltv expiry
@ -1166,7 +1156,7 @@ class Peer(PrintError):
self.print_error("commitment_signed", payload)
channel_id = payload['channel_id']
chan = self.channels[channel_id]
chan.local_state=chan.local_state._replace(
chan.config[LOCAL]=chan.config[LOCAL]._replace(
current_commitment_signature=payload['signature'],
current_htlc_signatures=payload['htlc_signature'])
self.lnworker.save_channel(chan)

258
electrum/lnhtlc.py

@ -10,7 +10,7 @@ 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, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction
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
@ -75,59 +75,50 @@ class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', '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']}
kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()}
return super().__new__(cls, **kwargs)
is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
def maybeDecode(k, v):
assert type(v) is not list
if 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:
return binascii.unhexlify(v)
return v
def decodeAll(v):
return {i: maybeDecode(i, j) for i, j in v.items()} if isinstance(v, dict) else v
def typeWrap(k, v, local):
if is_key(k):
if local:
return Keypair(**v)
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:
return OnlyPubkeyKeypair(**v)
return v
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):
self.local_config = state["local_config"]
if type(self.local_config) is not ChannelConfig:
new_local_config = {k: typeWrap(k, decodeAll(v), True) for k, v in self.local_config.items()}
self.local_config = ChannelConfig(**new_local_config)
self.remote_config = state["remote_config"]
if type(self.remote_config) is not ChannelConfig:
new_remote_config = {k: typeWrap(k, decodeAll(v), False) for k, v in self.remote_config.items()}
self.remote_config = ChannelConfig(**new_remote_config)
self.local_state = state["local_state"]
if type(self.local_state) is not LocalState:
self.local_state = LocalState(**decodeAll(self.local_state))
self.remote_state = state["remote_state"]
if type(self.remote_state) is not RemoteState:
self.remote_state = RemoteState(**decodeAll(self.remote_state))
if type(self.remote_state.revocation_store) is not RevocationStore:
self.remote_state = self.remote_state._replace(revocation_store = RevocationStore.from_json_obj(self.remote_state.revocation_store))
self.channel_id = maybeDecode("channel_id", state["channel_id"]) if type(state["channel_id"]) is not bytes else state["channel_id"]
self.constraints = ChannelConstraints(**decodeAll(state["constraints"])) if type(state["constraints"]) is not ChannelConstraints else state["constraints"]
self.funding_outpoint = Outpoint(**decodeAll(state["funding_outpoint"])) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"]
self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"]
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 {}
@ -141,7 +132,7 @@ class HTLCStateMachine(PrintError):
for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
if strname not in state: continue
for y in state[strname]:
htlc = UpdateAddHtlc(*decodeAll(y))
htlc = UpdateAddHtlc(**y)
self.log[subject]['adds'][htlc.htlc_id] = htlc
self.name = name
@ -172,7 +163,7 @@ class HTLCStateMachine(PrintError):
return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED'
def get_funding_address(self):
script = funding_output_script(self.local_config, self.remote_config)
script = funding_output_script(self.config[LOCAL], self.config[REMOTE])
return redeem_script_to_address('p2wsh', script)
def add_htlc(self, htlc):
@ -181,10 +172,10 @@ class HTLCStateMachine(PrintError):
should be called when preparing to send an outgoing HTLC.
"""
assert type(htlc) is dict
htlc = UpdateAddHtlc(**htlc, htlc_id=self.local_state.next_htlc_id)
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.local_state=self.local_state._replace(next_htlc_id=htlc.htlc_id + 1)
self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1)
return htlc.htlc_id
def receive_htlc(self, htlc):
@ -194,10 +185,10 @@ class HTLCStateMachine(PrintError):
party.
"""
assert type(htlc) is dict
htlc = UpdateAddHtlc(**htlc, htlc_id = self.remote_state.next_htlc_id)
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.remote_state=self.remote_state._replace(next_htlc_id=htlc.htlc_id + 1)
self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1)
return htlc.htlc_id
def sign_next_commitment(self):
@ -215,15 +206,15 @@ class HTLCStateMachine(PrintError):
"""
for htlc in self.log[LOCAL]['adds'].values():
if htlc.locked_in[LOCAL] is None:
htlc.locked_in[LOCAL] = self.local_state.ctn
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.local_config, self.remote_config)
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.local_config.htlc_basepoint.privkey, 'big'),
self.remote_state.next_per_commitment_point)
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
@ -231,7 +222,7 @@ class HTLCStateMachine(PrintError):
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.remote_state.next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc]
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])
@ -265,13 +256,13 @@ class HTLCStateMachine(PrintError):
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.remote_state.ctn
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.remote_config.multisig_key.pubkey, sig, pre_hash):
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
@ -280,7 +271,7 @@ class HTLCStateMachine(PrintError):
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.remote_config.htlc_basepoint.pubkey, this_point)
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]
@ -314,8 +305,8 @@ class HTLCStateMachine(PrintError):
last_secret, this_point, next_point = self.points
new_local_feerate = self.local_state.feerate
new_remote_feerate = self.remote_state.feerate
new_local_feerate = self.config[LOCAL].feerate
new_remote_feerate = self.config[REMOTE].feerate
for pending_fee in self.fee_mgr[:]:
if not self.constraints.is_initiator and pending_fee.had(FUNDEE_SIGNED):
@ -327,11 +318,11 @@ class HTLCStateMachine(PrintError):
self.fee_mgr.remove(pending_fee)
print("FEERATE CHANGE COMPLETE (initiator)")
self.local_state=self.local_state._replace(
ctn=self.local_state.ctn + 1,
self.config[LOCAL]=self.config[LOCAL]._replace(
ctn=self.config[LOCAL].ctn + 1,
feerate=new_local_feerate
)
self.remote_state=self.remote_state._replace(
self.config[REMOTE]=self.config[REMOTE]._replace(
feerate=new_remote_feerate
)
@ -341,13 +332,13 @@ class HTLCStateMachine(PrintError):
@property
def points(self):
last_small_num = self.local_state.ctn
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.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num)
this_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num)
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.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num)
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
@ -358,13 +349,13 @@ class HTLCStateMachine(PrintError):
return
outpoint = self.funding_outpoint.to_str()
if ours:
ctn = self.local_state.ctn + 1
ctn = self.config[LOCAL].ctn + 1
our_per_commitment_secret = get_per_commitment_secret_from_seed(
self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
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.remote_state.next_per_commitment_point
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)
@ -390,7 +381,7 @@ class HTLCStateMachine(PrintError):
"""
self.print_error("receive_revocation")
cur_point = self.remote_state.current_per_commitment_point
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')
@ -400,14 +391,14 @@ class HTLCStateMachine(PrintError):
# this might break
prev_remote_commitment = self.pending_remote_commitment
self.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
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 = self.htlcsum(self.gen_htlc_indices(subject, False))
old_amount = htlcsum(self.htlcs(subject, False))
for htlc_id in self.log[-subject]['settles']:
adds = self.log[subject]['adds']
@ -415,23 +406,23 @@ class HTLCStateMachine(PrintError):
self.settled[subject].append(htlc.amount_msat)
self.log[-subject]['settles'].clear()
return old_amount - self.htlcsum(self.gen_htlc_indices(subject, False))
return old_amount - htlcsum(self.htlcs(subject, False))
sent_this_batch = mark_settled(LOCAL)
received_this_batch = mark_settled(REMOTE)
next_point = self.remote_state.next_per_commitment_point
next_point = self.config[REMOTE].next_per_commitment_point
print("RECEIVED", received_this_batch)
print("SENT", sent_this_batch)
self.remote_state=self.remote_state._replace(
ctn=self.remote_state.ctn + 1,
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.remote_state.amount_msat + (sent_this_batch - received_this_batch)
amount_msat=self.config[REMOTE].amount_msat + (sent_this_batch - received_this_batch)
)
self.local_state=self.local_state._replace(
amount_msat = self.local_state.amount_msat + (received_this_batch - sent_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:
@ -444,43 +435,39 @@ class HTLCStateMachine(PrintError):
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
initial = self.config[subject].initial_msat
initial -= sum(self.settled[subject])
initial += sum(self.settled[-subject])
assert initial == (self.local_state.amount_msat if subject == LOCAL else self.remote_state.amount_msat)
assert initial == self.config[subject].amount_msat
return initial
@staticmethod
def htlcsum(htlcs):
amount_unsettled = 0
for x in htlcs:
amount_unsettled += x.amount_msat
return amount_unsettled
def amounts(self):
remote_settled= self.htlcsum(self.gen_htlc_indices(REMOTE, False))
local_settled= self.htlcsum(self.gen_htlc_indices(LOCAL, False))
unsettled_local = self.htlcsum(self.gen_htlc_indices(LOCAL, True))
unsettled_remote = self.htlcsum(self.gen_htlc_indices(REMOTE, True))
remote_msat = self.remote_state.amount_msat -\
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.local_state.amount_msat -\
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.remote_config if subject == REMOTE else self.local_config
conf = self.config[subject]
weight = HTLC_SUCCESS_WEIGHT if subject != htlc_initiator else HTLC_TIMEOUT_WEIGHT
htlcs = self.htlcs_in_local if htlc_initiator == LOCAL else self.htlcs_in_remote
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.remote_state.next_per_commitment_point
this_point = self.config[REMOTE].next_per_commitment_point
return self.make_commitment(REMOTE, this_point)
def pending_feerate(self, subject):
@ -495,7 +482,7 @@ class HTLCStateMachine(PrintError):
@property
def _committed_feerate(self):
return {LOCAL: self.local_state.feerate, REMOTE: self.remote_state.feerate}
return {LOCAL: self.config[LOCAL].feerate, REMOTE: self.config[REMOTE].feerate}
@property
def pending_local_commitment(self):
@ -505,10 +492,9 @@ class HTLCStateMachine(PrintError):
def total_msat(self, sub):
return sum(self.settled[sub])
def gen_htlc_indices(self, subject, only_pending):
def htlcs(self, subject, only_pending):
"""
only_pending: require the htlc's settlement to be pending (needs additional signatures/acks)
include_settled: include settled (totally done with) htlcs
"""
update_log = self.log[subject]
other_log = self.log[-subject]
@ -521,16 +507,6 @@ class HTLCStateMachine(PrintError):
res.append(htlc)
return res
@property
def htlcs_in_local(self):
"""in the local log. 'offered by us'"""
return self.gen_htlc_indices(LOCAL, True)
@property
def htlcs_in_remote(self):
"""in the remote log. 'offered by them'"""
return self.gen_htlc_indices(REMOTE, True)
def settle_htlc(self, preimage, htlc_id):
"""
SettleHTLC attempts to settle an existing outstanding received HTLC.
@ -552,7 +528,7 @@ class HTLCStateMachine(PrintError):
@property
def current_height(self):
return {LOCAL: self.local_state.ctn, REMOTE: self.remote_state.ctn}
return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn}
@property
def pending_local_fee(self):
@ -581,7 +557,7 @@ class HTLCStateMachine(PrintError):
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)
htlcs.append(i._asdict())
else:
removed.append(i.htlc_id)
return htlcs, removed
@ -593,10 +569,8 @@ class HTLCStateMachine(PrintError):
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.local_config,
"remote_config": self.remote_config,
"local_state": self.local_state,
"remote_state": self.remote_state,
"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,
@ -613,12 +587,12 @@ class HTLCStateMachine(PrintError):
# 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_state'].next_htlc_id
to_save['remote_state'] = to_save['remote_state']._replace(next_htlc_id = min(remote_removed))
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_state'].next_htlc_id
to_save['local_state'] = to_save['local_state']._replace(next_htlc_id = min(local_removed))
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
@ -652,8 +626,8 @@ class HTLCStateMachine(PrintError):
remote_msat, local_msat = self.amounts()
assert local_msat >= 0
assert remote_msat >= 0
this_config = self.remote_config if subject != LOCAL else self.local_config
other_config = self.remote_config if subject == LOCAL else self.local_config
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)
@ -676,12 +650,12 @@ class HTLCStateMachine(PrintError):
remote_msat, local_msat = local_msat, remote_msat
payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
return make_commitment(
(self.local_state.ctn if subject == LOCAL else self.remote_state.ctn) + 1,
self.config[subject].ctn + 1,
this_config.multisig_key.pubkey,
other_config.multisig_key.pubkey,
payment_pubkey,
self.local_config.payment_basepoint.pubkey,
self.remote_config.payment_basepoint.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,
@ -700,28 +674,28 @@ class HTLCStateMachine(PrintError):
fee_sat = self.pending_local_fee
_, outputs = make_outputs(fee_sat * 1000, True,
self.local_state.amount_msat,
self.remote_state.amount_msat,
self.config[LOCAL].amount_msat,
self.config[REMOTE].amount_msat,
(TYPE_SCRIPT, bh2u(local_script)),
(TYPE_SCRIPT, bh2u(remote_script)),
[], self.local_config.dust_limit_sat)
[], self.config[LOCAL].dust_limit_sat)
closing_tx = make_closing_tx(self.local_config.multisig_key.pubkey,
self.remote_config.multisig_key.pubkey,
self.local_config.payment_basepoint.pubkey,
self.remote_config.payment_basepoint.pubkey,
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.local_config.multisig_key.privkey))
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.local_config.payment_basepoint.privkey)
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)
@ -742,11 +716,11 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret
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.local_config.revocation_basepoint.privkey,
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.local_config.to_self_delay
delayed_pubkey = derive_pubkey(chan.remote_config.delayed_basepoint.pubkey,
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))
@ -768,13 +742,13 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret
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.local_config.delayed_basepoint.privkey)
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.remote_config.revocation_basepoint.pubkey,
revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey,
our_pcp)
to_self_delay = chan.remote_config.to_self_delay
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)

48
electrum/lnutil.py

@ -20,14 +20,36 @@ HTLC_TIMEOUT_WEIGHT = 663
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", "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"])
common = [
('ctn' , int),
('amount_msat' , int),
('next_htlc_id' , int),
('feerate' , int),
('payment_basepoint' , Keypair),
('multisig_key' , Keypair),
('htlc_basepoint' , Keypair),
('delayed_basepoint' , Keypair),
('revocation_basepoint' , Keypair),
('to_self_delay' , int),
('dust_limit_sat' , int),
('max_htlc_value_in_flight_msat' , int),
('max_accepted_htlcs' , int),
('initial_msat' , int),
]
ChannelConfig = NamedTuple('ChannelConfig', common)
LocalConfig = NamedTuple('LocalConfig', common + [
('per_commitment_secret_seed', bytes),
('funding_locked_received', bool),
('was_announced', bool),
('current_commitment_signature', bytes),
('current_htlc_signatures', List[bytes]),
])
ChannelConstraints = namedtuple("ChannelConstraints", ["capacity", "is_initiator", "funding_txn_minimum_depth"])
#OpenChannel = namedtuple("OpenChannel", ["channel_id", "short_channel_id", "funding_outpoint", "local_config", "remote_config", "remote_state", "local_state", "constraints", "node_id"])
ScriptHtlc = namedtuple('ScriptHtlc', ['redeem_script', 'htlc'])
@ -88,6 +110,12 @@ class RevocationStore:
def __hash__(self):
return hash(json.dumps(self.serialize(), sort_keys=True))
RemoteConfig = NamedTuple('RemoteConfig', common + [
('next_per_commitment_point' , bytes),
('revocation_store' , RevocationStore),
('current_per_commitment_point' , bytes),
])
def count_trailing_zeros(index):
""" BOLT-03 (where_to_put_secret) """
try:
@ -243,8 +271,8 @@ def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, p
def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc):
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
conf = chan.local_config if for_us else chan.remote_config
other_conf = chan.local_config if not for_us else chan.remote_config
conf = chan.config[LOCAL] if for_us else chan.config[REMOTE]
other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE]
revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp)
@ -412,8 +440,8 @@ def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes,
return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
def extract_ctn_from_tx_and_chan(tx, chan) -> int:
funder_conf = chan.local_config if chan.constraints.is_initiator else chan.remote_config
fundee_conf = chan.local_config if not chan.constraints.is_initiator else chan.remote_config
funder_conf = chan.config[LOCAL] if chan.constraints.is_initiator else chan.config[REMOTE]
fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE]
return extract_ctn_from_tx(tx, txin_index=0,
funder_payment_basepoint=funder_conf.payment_basepoint.pubkey,
fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey)

12
electrum/lnworker.py

@ -116,7 +116,7 @@ class LNWorker(PrintError):
def save_channel(self, openchannel):
assert type(openchannel) is HTLCStateMachine
if openchannel.remote_state.next_per_commitment_point == openchannel.remote_state.current_per_commitment_point:
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:
self.channels[openchannel.channel_id] = openchannel
@ -350,12 +350,12 @@ class LNWorker(PrintError):
chan = self.channels[chan_id]
# local_commitment always gives back the next expected local_commitment,
# but in this case, we want the current one. So substract one ctn number
old_local_state = chan.local_state
chan.local_state=chan.local_state._replace(ctn=chan.local_state.ctn - 1)
old_local_state = chan.config[LOCAL]
chan.config[LOCAL]=chan.config[LOCAL]._replace(ctn=chan.config[LOCAL].ctn - 1)
tx = chan.pending_local_commitment
chan.local_state = old_local_state
tx.sign({bh2u(chan.local_config.multisig_key.pubkey): (chan.local_config.multisig_key.privkey, True)})
remote_sig = chan.local_state.current_commitment_signature
chan.config[LOCAL] = old_local_state
tx.sign({bh2u(chan.config[LOCAL].multisig_key.pubkey): (chan.config[LOCAL].multisig_key.privkey, True)})
remote_sig = chan.config[LOCAL].current_commitment_signature
remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01"
none_idx = tx._inputs[0]["signatures"].index(None)
tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))

90
electrum/tests/test_lnhtlc.py

@ -16,56 +16,52 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
assert remote_amount > 0
channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
their_revocation_store = lnbase.RevocationStore()
local_config=lnbase.ChannelConfig(
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,
)
remote_config=lnbase.ChannelConfig(
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,
)
return {
"channel_id":channel_id,
"short_channel_id":channel_id[:8],
"funding_outpoint":lnbase.Outpoint(funding_txid, funding_index),
"local_config":local_config,
"remote_config":remote_config,
"remote_state":lnbase.RemoteState(
"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,
feerate=local_feerate,
amount_msat=remote_amount,
next_per_commitment_point=nex,
current_per_commitment_point=cur,
amount_msat=remote_amount,
revocation_store=their_revocation_store,
next_htlc_id = 0,
feerate=local_feerate
),
"local_state":lnbase.LocalState(
"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,
per_commitment_secret_seed=seed,
amount_msat=local_amount,
next_htlc_id = 0,
feerate=local_feerate,
amount_msat=local_amount,
per_commitment_secret_seed=seed,
funding_locked_received=True,
was_announced=False,
current_commitment_signature=None,
current_htlc_signatures=None,
feerate=local_feerate
),
"constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3),
"node_id":other_node_id,
@ -205,8 +201,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
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.local_state.ctn, 1, "bob has incorrect commitment height")
self.assertEqual(alice_channel.local_state.ctn, 1, "alice has incorrect commitment height")
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.
@ -275,20 +271,20 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
self.assertNotEqual(fee, bob_channel.local_state.feerate)
self.assertNotEqual(fee, bob_channel.config[LOCAL].feerate)
rev, _ = bob_channel.revoke_current_commitment()
self.assertEqual(fee, bob_channel.local_state.feerate)
self.assertEqual(fee, bob_channel.config[LOCAL].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.local_state.feerate)
self.assertNotEqual(fee, alice_channel.config[LOCAL].feerate)
rev, _ = alice_channel.revoke_current_commitment()
self.assertEqual(fee, alice_channel.local_state.feerate)
self.assertEqual(fee, alice_channel.config[LOCAL].feerate)
bob_channel.receive_revocation(rev)
self.assertEqual(fee, bob_channel.remote_state.feerate)
self.assertEqual(fee, bob_channel.config[REMOTE].feerate)
def test_UpdateFeeReceiverCommits(self):
@ -304,20 +300,20 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
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.local_state.feerate)
self.assertNotEqual(fee, bob_channel.config[LOCAL].feerate)
bob_revocation, _ = bob_channel.revoke_current_commitment()
self.assertEqual(fee, bob_channel.local_state.feerate)
self.assertEqual(fee, bob_channel.config[LOCAL].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.local_state.feerate)
self.assertNotEqual(fee, alice_channel.config[LOCAL].feerate)
alice_revocation, _ = alice_channel.revoke_current_commitment()
self.assertEqual(fee, alice_channel.local_state.feerate)
self.assertEqual(fee, alice_channel.config[LOCAL].feerate)
bob_channel.receive_revocation(alice_revocation)
self.assertEqual(fee, bob_channel.remote_state.feerate)
self.assertEqual(fee, bob_channel.config[REMOTE].feerate)
@ -327,7 +323,7 @@ class TestLNHTLCDust(unittest.TestCase):
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
fee_per_kw = alice_channel.local_state.feerate
fee_per_kw = alice_channel.config[LOCAL].feerate
self.assertEqual(fee_per_kw, 6000)
htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
self.assertEqual(htlcAmt, 4478)

Loading…
Cancel
Save