Browse Source

lnhtlc: merge config and state, remove unnecessary properties

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

256
electrum/lnhtlc.py

@ -10,7 +10,7 @@ from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS
from .bitcoin import redeem_script_to_address from .bitcoin import redeem_script_to_address
from .crypto import sha256 from .crypto import sha256
from . import ecc 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 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 make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script
from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey from .lnutil import 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: if 'locked_in' not in kwargs:
kwargs['locked_in'] = {LOCAL: None, REMOTE: None} kwargs['locked_in'] = {LOCAL: None, REMOTE: None}
else: 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) return super().__new__(cls, **kwargs)
is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key") def decodeAll(d, local):
for k, v in d.items():
def maybeDecode(k, v): if k == 'revocation_store':
assert type(v) is not list yield (k, RevocationStore.from_json_obj(v))
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: elif k.endswith("_basepoint") or k.endswith("_key"):
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: if local:
return Keypair(**v) yield (k, Keypair(**dict(decodeAll(v, local))))
else: else:
return OnlyPubkeyKeypair(**v) yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local))))
return v elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None:
yield (k, binascii.unhexlify(v))
else:
yield (k, v)
def htlcsum(htlcs):
return sum([x.amount_msat for x in htlcs])
class HTLCStateMachine(PrintError): class HTLCStateMachine(PrintError):
def diagnostic_name(self): def diagnostic_name(self):
return str(self.name) return str(self.name)
def __init__(self, state, name = None): def __init__(self, state, name = None):
self.local_config = state["local_config"] assert 'local_state' not in state
if type(self.local_config) is not ChannelConfig: self.config = {}
new_local_config = {k: typeWrap(k, decodeAll(v), True) for k, v in self.local_config.items()} self.config[LOCAL] = state["local_config"]
self.local_config = ChannelConfig(**new_local_config) if type(self.config[LOCAL]) is not LocalConfig:
conf = dict(decodeAll(self.config[LOCAL], True))
self.remote_config = state["remote_config"] self.config[LOCAL] = LocalConfig(**conf)
if type(self.remote_config) is not ChannelConfig: assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes
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.config[REMOTE] = state["remote_config"]
if type(self.config[REMOTE]) is not RemoteConfig:
self.local_state = state["local_state"] conf = dict(decodeAll(self.config[REMOTE], False))
if type(self.local_state) is not LocalState: self.config[REMOTE] = RemoteConfig(**conf)
self.local_state = LocalState(**decodeAll(self.local_state)) assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes
self.remote_state = state["remote_state"] self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"]
if type(self.remote_state) is not RemoteState: self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"]
self.remote_state = RemoteState(**decodeAll(self.remote_state)) 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"]
if type(self.remote_state.revocation_store) is not RevocationStore: 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.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"]
self.short_channel_id_predicted = self.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 {} 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)]: for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
if strname not in state: continue if strname not in state: continue
for y in state[strname]: for y in state[strname]:
htlc = UpdateAddHtlc(*decodeAll(y)) htlc = UpdateAddHtlc(**y)
self.log[subject]['adds'][htlc.htlc_id] = htlc self.log[subject]['adds'][htlc.htlc_id] = htlc
self.name = name self.name = name
@ -172,7 +163,7 @@ class HTLCStateMachine(PrintError):
return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED' return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED'
def get_funding_address(self): 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) return redeem_script_to_address('p2wsh', script)
def add_htlc(self, htlc): def add_htlc(self, htlc):
@ -181,10 +172,10 @@ class HTLCStateMachine(PrintError):
should be called when preparing to send an outgoing HTLC. should be called when preparing to send an outgoing HTLC.
""" """
assert type(htlc) is dict 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.log[LOCAL]['adds'][htlc.htlc_id] = htlc
self.print_error("add_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 return htlc.htlc_id
def receive_htlc(self, htlc): def receive_htlc(self, htlc):
@ -194,10 +185,10 @@ class HTLCStateMachine(PrintError):
party. party.
""" """
assert type(htlc) is dict 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.log[REMOTE]['adds'][htlc.htlc_id] = htlc
self.print_error("receive_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 return htlc.htlc_id
def sign_next_commitment(self): def sign_next_commitment(self):
@ -215,15 +206,15 @@ class HTLCStateMachine(PrintError):
""" """
for htlc in self.log[LOCAL]['adds'].values(): for htlc in self.log[LOCAL]['adds'].values():
if htlc.locked_in[LOCAL] is None: 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") self.print_error("sign_next_commitment")
pending_remote_commitment = self.pending_remote_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( their_remote_htlc_privkey_number = derive_privkey(
int.from_bytes(self.local_config.htlc_basepoint.privkey, 'big'), int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'),
self.remote_state.next_per_commitment_point) self.config[REMOTE].next_per_commitment_point)
their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big') their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
for_us = False for_us = False
@ -231,7 +222,7 @@ class HTLCStateMachine(PrintError):
htlcsigs = [] htlcsigs = []
for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]): for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]):
for htlc in htlcs: 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) htlc_tx = make_htlc_tx_with_open_channel(self, *args)
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
@ -265,13 +256,13 @@ class HTLCStateMachine(PrintError):
self.print_error("receive_new_commitment") self.print_error("receive_new_commitment")
for htlc in self.log[REMOTE]['adds'].values(): for htlc in self.log[REMOTE]['adds'].values():
if htlc.locked_in[REMOTE] is None: 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 assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
pending_local_commitment = self.pending_local_commitment pending_local_commitment = self.pending_local_commitment
preimage_hex = pending_local_commitment.serialize_preimage(0) preimage_hex = pending_local_commitment.serialize_preimage(0)
pre_hash = Hash(bfh(preimage_hex)) 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) raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex)
_, this_point, _ = self.points _, this_point, _ = self.points
@ -280,7 +271,7 @@ class HTLCStateMachine(PrintError):
for htlc in htlcs: for htlc in htlcs:
htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc) 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))) 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): for idx, sig in enumerate(htlc_sigs):
if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
del htlc_sigs[idx] del htlc_sigs[idx]
@ -314,8 +305,8 @@ class HTLCStateMachine(PrintError):
last_secret, this_point, next_point = self.points last_secret, this_point, next_point = self.points
new_local_feerate = self.local_state.feerate new_local_feerate = self.config[LOCAL].feerate
new_remote_feerate = self.remote_state.feerate new_remote_feerate = self.config[REMOTE].feerate
for pending_fee in self.fee_mgr[:]: for pending_fee in self.fee_mgr[:]:
if not self.constraints.is_initiator and pending_fee.had(FUNDEE_SIGNED): 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) self.fee_mgr.remove(pending_fee)
print("FEERATE CHANGE COMPLETE (initiator)") print("FEERATE CHANGE COMPLETE (initiator)")
self.local_state=self.local_state._replace( self.config[LOCAL]=self.config[LOCAL]._replace(
ctn=self.local_state.ctn + 1, ctn=self.config[LOCAL].ctn + 1,
feerate=new_local_feerate feerate=new_local_feerate
) )
self.remote_state=self.remote_state._replace( self.config[REMOTE]=self.config[REMOTE]._replace(
feerate=new_remote_feerate feerate=new_remote_feerate
) )
@ -341,13 +332,13 @@ class HTLCStateMachine(PrintError):
@property @property
def points(self): def points(self):
last_small_num = self.local_state.ctn last_small_num = self.config[LOCAL].ctn
this_small_num = last_small_num + 1 this_small_num = last_small_num + 1
next_small_num = last_small_num + 2 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) 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.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - this_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')) 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')) next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big'))
return last_secret, this_point, next_point return last_secret, this_point, next_point
@ -358,13 +349,13 @@ class HTLCStateMachine(PrintError):
return return
outpoint = self.funding_outpoint.to_str() outpoint = self.funding_outpoint.to_str()
if ours: if ours:
ctn = self.local_state.ctn + 1 ctn = self.config[LOCAL].ctn + 1
our_per_commitment_secret = get_per_commitment_secret_from_seed( 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) 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) encumbered_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address)
else: 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) 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) self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx)
@ -390,7 +381,7 @@ class HTLCStateMachine(PrintError):
""" """
self.print_error("receive_revocation") 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) derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True)
if cur_point != derived_point: if cur_point != derived_point:
raise Exception('revoked secret not for current point') raise Exception('revoked secret not for current point')
@ -400,14 +391,14 @@ class HTLCStateMachine(PrintError):
# this might break # this might break
prev_remote_commitment = self.pending_remote_commitment 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) self.process_new_revocation_secret(revocation.per_commitment_secret)
def mark_settled(subject): def mark_settled(subject):
""" """
find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs 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']: for htlc_id in self.log[-subject]['settles']:
adds = self.log[subject]['adds'] adds = self.log[subject]['adds']
@ -415,23 +406,23 @@ class HTLCStateMachine(PrintError):
self.settled[subject].append(htlc.amount_msat) self.settled[subject].append(htlc.amount_msat)
self.log[-subject]['settles'].clear() 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) sent_this_batch = mark_settled(LOCAL)
received_this_batch = mark_settled(REMOTE) 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("RECEIVED", received_this_batch)
print("SENT", sent_this_batch) print("SENT", sent_this_batch)
self.remote_state=self.remote_state._replace( self.config[REMOTE]=self.config[REMOTE]._replace(
ctn=self.remote_state.ctn + 1, ctn=self.config[REMOTE].ctn + 1,
current_per_commitment_point=next_point, current_per_commitment_point=next_point,
next_per_commitment_point=revocation.next_per_commitment_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( self.config[LOCAL]=self.config[LOCAL]._replace(
amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch) amount_msat = self.config[LOCAL].amount_msat + (received_this_batch - sent_this_batch)
) )
for pending_fee in self.fee_mgr: for pending_fee in self.fee_mgr:
@ -444,43 +435,39 @@ class HTLCStateMachine(PrintError):
return received_this_batch, sent_this_batch return received_this_batch, sent_this_batch
def balance(self, subject): 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])
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 return initial
@staticmethod
def htlcsum(htlcs):
amount_unsettled = 0
for x in htlcs:
amount_unsettled += x.amount_msat
return amount_unsettled
def amounts(self): def amounts(self):
remote_settled= self.htlcsum(self.gen_htlc_indices(REMOTE, False)) remote_settled= htlcsum(self.htlcs(REMOTE, False))
local_settled= self.htlcsum(self.gen_htlc_indices(LOCAL, False)) local_settled= htlcsum(self.htlcs(LOCAL, False))
unsettled_local = self.htlcsum(self.gen_htlc_indices(LOCAL, True)) unsettled_local = htlcsum(self.htlcs(LOCAL, True))
unsettled_remote = self.htlcsum(self.gen_htlc_indices(REMOTE, True)) unsettled_remote = htlcsum(self.htlcs(REMOTE, True))
remote_msat = self.remote_state.amount_msat -\ remote_msat = self.config[REMOTE].amount_msat -\
unsettled_remote + local_settled - remote_settled 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 unsettled_local + remote_settled - local_settled
return remote_msat, local_msat return remote_msat, local_msat
def included_htlcs(self, subject, htlc_initiator): 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) 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 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) fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000)
return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs) return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs)
@property @property
def pending_remote_commitment(self): 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) return self.make_commitment(REMOTE, this_point)
def pending_feerate(self, subject): def pending_feerate(self, subject):
@ -495,7 +482,7 @@ class HTLCStateMachine(PrintError):
@property @property
def _committed_feerate(self): 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 @property
def pending_local_commitment(self): def pending_local_commitment(self):
@ -505,10 +492,9 @@ class HTLCStateMachine(PrintError):
def total_msat(self, sub): def total_msat(self, sub):
return sum(self.settled[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) 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] update_log = self.log[subject]
other_log = self.log[-subject] other_log = self.log[-subject]
@ -521,16 +507,6 @@ class HTLCStateMachine(PrintError):
res.append(htlc) res.append(htlc)
return res 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): def settle_htlc(self, preimage, htlc_id):
""" """
SettleHTLC attempts to settle an existing outstanding received HTLC. SettleHTLC attempts to settle an existing outstanding received HTLC.
@ -552,7 +528,7 @@ class HTLCStateMachine(PrintError):
@property @property
def current_height(self): 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 @property
def pending_local_fee(self): def pending_local_fee(self):
@ -581,7 +557,7 @@ class HTLCStateMachine(PrintError):
for i in self.log[subject]['adds'].values(): 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 locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None
if locked_in: if locked_in:
htlcs.append(i) htlcs.append(i._asdict())
else: else:
removed.append(i.htlc_id) removed.append(i.htlc_id)
return htlcs, removed return htlcs, removed
@ -593,10 +569,8 @@ class HTLCStateMachine(PrintError):
remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE) remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE)
local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL) local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL)
to_save = { to_save = {
"local_config": self.local_config, "local_config": self.config[LOCAL],
"remote_config": self.remote_config, "remote_config": self.config[REMOTE],
"local_state": self.local_state,
"remote_state": self.remote_state,
"channel_id": self.channel_id, "channel_id": self.channel_id,
"short_channel_id": self.short_channel_id, "short_channel_id": self.short_channel_id,
"constraints": self.constraints, "constraints": self.constraints,
@ -613,12 +587,12 @@ class HTLCStateMachine(PrintError):
# htlcs number must be monotonically increasing, # htlcs number must be monotonically increasing,
# so we have to decrease the counter # so we have to decrease the counter
if len(remote_removed) != 0: if len(remote_removed) != 0:
assert min(remote_removed) < to_save['remote_state'].next_htlc_id assert min(remote_removed) < to_save['remote_config'].next_htlc_id
to_save['remote_state'] = to_save['remote_state']._replace(next_htlc_id = min(remote_removed)) to_save['remote_config'] = to_save['remote_config']._replace(next_htlc_id = min(remote_removed))
if len(local_removed) != 0: if len(local_removed) != 0:
assert min(local_removed) < to_save['local_state'].next_htlc_id assert min(local_removed) < to_save['local_config'].next_htlc_id
to_save['local_state'] = to_save['local_state']._replace(next_htlc_id = min(local_removed)) to_save['local_config'] = to_save['local_config']._replace(next_htlc_id = min(local_removed))
return to_save return to_save
@ -652,8 +626,8 @@ class HTLCStateMachine(PrintError):
remote_msat, local_msat = self.amounts() remote_msat, local_msat = self.amounts()
assert local_msat >= 0 assert local_msat >= 0
assert remote_msat >= 0 assert remote_msat >= 0
this_config = self.remote_config if subject != LOCAL else self.local_config this_config = self.config[subject]
other_config = self.remote_config if subject == LOCAL else self.local_config other_config = self.config[-subject]
other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point) other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)
this_htlc_pubkey = derive_pubkey(this_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) 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 remote_msat, local_msat = local_msat, remote_msat
payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point) payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
return make_commitment( 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, this_config.multisig_key.pubkey,
other_config.multisig_key.pubkey, other_config.multisig_key.pubkey,
payment_pubkey, payment_pubkey,
self.local_config.payment_basepoint.pubkey, self.config[LOCAL].payment_basepoint.pubkey,
self.remote_config.payment_basepoint.pubkey, self.config[REMOTE].payment_basepoint.pubkey,
other_revocation_pubkey, other_revocation_pubkey,
derive_pubkey(this_config.delayed_basepoint.pubkey, this_point), derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),
other_config.to_self_delay, other_config.to_self_delay,
@ -700,28 +674,28 @@ class HTLCStateMachine(PrintError):
fee_sat = self.pending_local_fee fee_sat = self.pending_local_fee
_, outputs = make_outputs(fee_sat * 1000, True, _, outputs = make_outputs(fee_sat * 1000, True,
self.local_state.amount_msat, self.config[LOCAL].amount_msat,
self.remote_state.amount_msat, self.config[REMOTE].amount_msat,
(TYPE_SCRIPT, bh2u(local_script)), (TYPE_SCRIPT, bh2u(local_script)),
(TYPE_SCRIPT, bh2u(remote_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, closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
self.remote_config.multisig_key.pubkey, self.config[REMOTE].multisig_key.pubkey,
self.local_config.payment_basepoint.pubkey, self.config[LOCAL].payment_basepoint.pubkey,
self.remote_config.payment_basepoint.pubkey, self.config[REMOTE].payment_basepoint.pubkey,
# TODO hardcoded we_are_initiator: # TODO hardcoded we_are_initiator:
True, *self.funding_outpoint, self.constraints.capacity, True, *self.funding_outpoint, self.constraints.capacity,
outputs) 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]) sig = ecc.sig_string_from_der_sig(der_sig[:-1])
return sig, fee_sat return sig, fee_sat
def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes, def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]: sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(their_pcp, bytes) 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 = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey) our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) 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]: sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(per_commitment_secret, bytes) assert isinstance(per_commitment_secret, bytes)
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) 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) per_commitment_secret)
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True) revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
to_self_delay = chan.local_config.to_self_delay to_self_delay = chan.config[LOCAL].to_self_delay
delayed_pubkey = derive_pubkey(chan.remote_config.delayed_basepoint.pubkey, delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey,
per_commitment_point) per_commitment_point)
witness_script = bh2u(make_commitment_output_to_local_witness_script( witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, delayed_pubkey)) 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, def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]: sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(our_pcp, bytes) 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 = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey) our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True) 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) 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( witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, our_localdelayed_pubkey)) revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script) 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 HTLC_SUCCESS_WEIGHT = 703
Keypair = namedtuple("Keypair", ["pubkey", "privkey"]) 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"]) 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"]) 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']) ScriptHtlc = namedtuple('ScriptHtlc', ['redeem_script', 'htlc'])
@ -88,6 +110,12 @@ class RevocationStore:
def __hash__(self): def __hash__(self):
return hash(json.dumps(self.serialize(), sort_keys=True)) 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): def count_trailing_zeros(index):
""" BOLT-03 (where_to_put_secret) """ """ BOLT-03 (where_to_put_secret) """
try: 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): 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 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 conf = chan.config[LOCAL] if for_us else chan.config[REMOTE]
other_conf = chan.local_config if not for_us else chan.remote_config other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE]
revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp) revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
delayedpubkey = derive_pubkey(conf.delayed_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) return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
def extract_ctn_from_tx_and_chan(tx, chan) -> int: def extract_ctn_from_tx_and_chan(tx, chan) -> int:
funder_conf = chan.local_config if 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.local_config if not chan.constraints.is_initiator else chan.remote_config fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE]
return extract_ctn_from_tx(tx, txin_index=0, return extract_ctn_from_tx(tx, txin_index=0,
funder_payment_basepoint=funder_conf.payment_basepoint.pubkey, funder_payment_basepoint=funder_conf.payment_basepoint.pubkey,
fundee_payment_basepoint=fundee_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): def save_channel(self, openchannel):
assert type(openchannel) is HTLCStateMachine 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") raise Exception("Tried to save channel with next_point == current_point, this should not happen")
with self.lock: with self.lock:
self.channels[openchannel.channel_id] = openchannel self.channels[openchannel.channel_id] = openchannel
@ -350,12 +350,12 @@ class LNWorker(PrintError):
chan = self.channels[chan_id] chan = self.channels[chan_id]
# local_commitment always gives back the next expected local_commitment, # local_commitment always gives back the next expected local_commitment,
# but in this case, we want the current one. So substract one ctn number # but in this case, we want the current one. So substract one ctn number
old_local_state = chan.local_state old_local_state = chan.config[LOCAL]
chan.local_state=chan.local_state._replace(ctn=chan.local_state.ctn - 1) chan.config[LOCAL]=chan.config[LOCAL]._replace(ctn=chan.config[LOCAL].ctn - 1)
tx = chan.pending_local_commitment tx = chan.pending_local_commitment
chan.local_state = old_local_state chan.config[LOCAL] = old_local_state
tx.sign({bh2u(chan.local_config.multisig_key.pubkey): (chan.local_config.multisig_key.privkey, True)}) tx.sign({bh2u(chan.config[LOCAL].multisig_key.pubkey): (chan.config[LOCAL].multisig_key.privkey, True)})
remote_sig = chan.local_state.current_commitment_signature remote_sig = chan.config[LOCAL].current_commitment_signature
remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01" remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01"
none_idx = tx._inputs[0]["signatures"].index(None) none_idx = tx._inputs[0]["signatures"].index(None)
tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))

80
electrum/tests/test_lnhtlc.py

@ -16,19 +16,12 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
assert remote_amount > 0 assert remote_amount > 0
channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
their_revocation_store = lnbase.RevocationStore() their_revocation_store = lnbase.RevocationStore()
local_config=lnbase.ChannelConfig(
payment_basepoint=privkeys[0], return {
multisig_key=privkeys[1], "channel_id":channel_id,
htlc_basepoint=privkeys[2], "short_channel_id":channel_id[:8],
delayed_basepoint=privkeys[3], "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index),
revocation_basepoint=privkeys[4], "remote_config":lnbase.RemoteConfig(
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], payment_basepoint=other_pubkeys[0],
multisig_key=other_pubkeys[1], multisig_key=other_pubkeys[1],
htlc_basepoint=other_pubkeys[2], htlc_basepoint=other_pubkeys[2],
@ -39,33 +32,36 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
max_htlc_value_in_flight_msat=500000 * 1000, max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5, max_accepted_htlcs=5,
initial_msat=remote_amount, 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(
ctn = 0, ctn = 0,
next_htlc_id = 0,
feerate=local_feerate,
amount_msat=remote_amount,
next_per_commitment_point=nex, next_per_commitment_point=nex,
current_per_commitment_point=cur, current_per_commitment_point=cur,
amount_msat=remote_amount,
revocation_store=their_revocation_store, 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, ctn = 0,
per_commitment_secret_seed=seed,
amount_msat=local_amount,
next_htlc_id = 0, next_htlc_id = 0,
feerate=local_feerate,
amount_msat=local_amount,
per_commitment_secret_seed=seed,
funding_locked_received=True, funding_locked_received=True,
was_announced=False, was_announced=False,
current_commitment_signature=None, current_commitment_signature=None,
current_htlc_signatures=None, current_htlc_signatures=None,
feerate=local_feerate
), ),
"constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3), "constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3),
"node_id":other_node_id, "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(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(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.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(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height")
self.assertEqual(alice_channel.local_state.ctn, 1, "alice 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 # Both commitment transactions should have three outputs, and one of
# them should be exactly the amount of the HTLC. # 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) 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() 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() bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
alice_channel.receive_revocation(rev) alice_channel.receive_revocation(rev)
alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) 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() 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) 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): def test_UpdateFeeReceiverCommits(self):
@ -304,20 +300,20 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) 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() 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() bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
alice_channel.receive_revocation(bob_revocation) alice_channel.receive_revocation(bob_revocation)
alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) 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() 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) 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 paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage) 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) self.assertEqual(fee_per_kw, 6000)
htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
self.assertEqual(htlcAmt, 4478) self.assertEqual(htlcAmt, 4478)

Loading…
Cancel
Save