# Copyright (C) 2018 The Electrum developers
# Copyright (C) 2015-2018 The Lightning Network Developers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# API (method signatures and docstrings) partially copied from lnd
# 42de4400bff5105352d0552155f73589166d162b
import os
from collections import namedtuple , defaultdict
import binascii
import json
from enum import Enum , auto
from typing import Optional , Dict , List , Tuple , NamedTuple , Set , Callable , Iterable , Sequence
from . import ecc
from . util import bfh , PrintError , bh2u
from . bitcoin import TYPE_SCRIPT , TYPE_ADDRESS
from . bitcoin import redeem_script_to_address
from . crypto import sha256 , sha256d
from . simple_config import get_config
from . transaction import Transaction
from . lnutil import ( Outpoint , LocalConfig , RemoteConfig , Keypair , OnlyPubkeyKeypair , ChannelConstraints ,
get_per_commitment_secret_from_seed , secret_to_pubkey , derive_privkey , make_closing_tx ,
sign_and_get_sig_string , RevocationStore , derive_blinded_pubkey , Direction , derive_pubkey ,
make_htlc_tx_with_open_channel , make_commitment , make_received_htlc , make_offered_htlc ,
HTLC_TIMEOUT_WEIGHT , HTLC_SUCCESS_WEIGHT , extract_ctn_from_tx_and_chan , UpdateAddHtlc ,
funding_output_script , SENT , RECEIVED , LOCAL , REMOTE , HTLCOwner , make_commitment_outputs ,
ScriptHtlc , PaymentFailure , calc_onchain_fees , RemoteMisbehaving , make_htlc_output_witness_script )
from . lnsweep import create_sweeptxs_for_their_just_revoked_ctx
from . lnsweep import create_sweeptxs_for_our_latest_ctx , create_sweeptxs_for_their_latest_ctx
from . lnhtlc import HTLCManager
class ChannelJsonEncoder ( json . JSONEncoder ) :
def default ( self , o ) :
if isinstance ( o , bytes ) :
return binascii . hexlify ( o ) . decode ( " ascii " )
if isinstance ( o , RevocationStore ) :
return o . serialize ( )
if isinstance ( o , set ) :
return list ( o )
return super ( ) . default ( o )
RevokeAndAck = namedtuple ( " RevokeAndAck " , [ " per_commitment_secret " , " next_per_commitment_point " ] )
class FeeUpdateProgress ( Enum ) :
FUNDEE_SIGNED = auto ( )
FUNDEE_ACKED = auto ( )
FUNDER_SIGNED = auto ( )
FUNDEE_SIGNED = FeeUpdateProgress . FUNDEE_SIGNED
FUNDEE_ACKED = FeeUpdateProgress . FUNDEE_ACKED
FUNDER_SIGNED = FeeUpdateProgress . FUNDER_SIGNED
class FeeUpdate ( defaultdict ) :
def __init__ ( self , chan , rate ) :
super ( ) . __init__ ( lambda : False )
self . rate = rate
self . chan = chan
def pending_feerate ( self , subject ) :
if self [ FUNDEE_ACKED ] :
return self . rate
if subject == REMOTE and self . chan . constraints . is_initiator :
return self . rate
if subject == LOCAL and not self . chan . constraints . is_initiator :
return self . rate
# implicit return None
def decodeAll ( d , local ) :
for k , v in d . items ( ) :
if k == ' revocation_store ' :
yield ( k , RevocationStore . from_json_obj ( v ) )
elif k . endswith ( " _basepoint " ) or k . endswith ( " _key " ) :
if local :
yield ( k , Keypair ( * * dict ( decodeAll ( v , local ) ) ) )
else :
yield ( k , OnlyPubkeyKeypair ( * * dict ( decodeAll ( v , local ) ) ) )
elif k in [ " node_id " , " channel_id " , " short_channel_id " , " pubkey " , " privkey " , " current_per_commitment_point " , " next_per_commitment_point " , " per_commitment_secret_seed " , " current_commitment_signature " , " current_htlc_signatures " ] and v is not None :
yield ( k , binascii . unhexlify ( v ) )
else :
yield ( k , v )
def htlcsum ( htlcs ) :
return sum ( [ x . amount_msat for x in htlcs ] )
# following two functions are used because json
# doesn't store int keys and byte string values
def str_bytes_dict_from_save ( x ) :
return { int ( k ) : bfh ( v ) for k , v in x . items ( ) }
def str_bytes_dict_to_save ( x ) :
return { str ( k ) : bh2u ( v ) for k , v in x . items ( ) }
class Channel ( PrintError ) :
def diagnostic_name ( self ) :
if self . name :
return str ( self . name )
try :
return f " lnchannel_ { bh2u ( self . channel_id [ - 4 : ] ) } "
except :
return super ( ) . diagnostic_name ( )
def __init__ ( self , state , * , sweep_address = None , name = None ,
payment_completed : Optional [ Callable [ [ ' Channel ' , Direction , UpdateAddHtlc , bytes ] , None ] ] = None ) :
self . preimages = { }
if not payment_completed :
payment_completed = lambda this , x , y , z : None
self . sweep_address = sweep_address
self . payment_completed = payment_completed
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 = str_bytes_dict_from_save ( state . get ( ' onion_keys ' , { } ) )
# FIXME this is a tx serialised in the custom electrum partial tx format.
# we should not persist txns in this format. we should persist htlcs, and be able to derive
# any past commitment transaction and use that instead; until then...
self . remote_commitment_to_be_revoked = Transaction ( state [ " remote_commitment_to_be_revoked " ] )
self . remote_commitment_to_be_revoked . deserialize ( True )
self . hm = HTLCManager ( state . get ( ' log ' ) )
self . name = name
self . pending_fee = None
self . _is_funding_txo_spent = None # "don't know"
self . _state = None
if state . get ( ' force_closed ' , False ) :
self . set_state ( ' FORCE_CLOSING ' )
else :
self . set_state ( ' DISCONNECTED ' )
self . lnwatcher = None
self . local_commitment = None
self . remote_commitment = None
def get_payments ( self ) :
out = { }
for subject in LOCAL , REMOTE :
log = self . hm . log [ subject ]
for htlc_id , htlc in log . get ( ' adds ' , { } ) . items ( ) :
rhash = bh2u ( htlc . payment_hash )
status = ' settled ' if htlc_id in log . get ( ' settles ' , { } ) else ' inflight '
direction = SENT if subject is LOCAL else RECEIVED
out [ rhash ] = ( self . channel_id , htlc , direction , status )
return out
def set_local_commitment ( self , ctx ) :
ctn = extract_ctn_from_tx_and_chan ( ctx , self )
assert self . signature_fits ( ctx ) , ( self . hm . log [ LOCAL ] )
self . local_commitment = ctx
if self . sweep_address is not None :
self . local_sweeptxs = create_sweeptxs_for_our_latest_ctx ( self , self . local_commitment , self . sweep_address )
initial = os . path . join ( get_config ( ) . electrum_path ( ) , ' initial_commitment_tx ' )
tx = self . force_close_tx ( ) . serialize_to_network ( )
if not os . path . exists ( initial ) :
with open ( initial , ' w ' ) as f :
f . write ( tx )
def set_remote_commitment ( self ) :
self . remote_commitment = self . current_commitment ( REMOTE )
if self . sweep_address is not None :
self . remote_sweeptxs = create_sweeptxs_for_their_latest_ctx ( self , self . remote_commitment , self . sweep_address )
def set_state ( self , state : str ) :
if self . _state == ' FORCE_CLOSING ' :
assert state == ' FORCE_CLOSING ' , ' new state was not FORCE_CLOSING: ' + state
self . _state = state
def get_state ( self ) :
return self . _state
def is_closed ( self ) :
return self . get_state ( ) in [ ' CLOSED ' , ' FORCE_CLOSING ' ]
def _check_can_pay ( self , amount_msat : int ) - > None :
if self . get_state ( ) != ' OPEN ' :
raise PaymentFailure ( ' Channel not open ' )
if self . available_to_spend ( LOCAL ) < amount_msat :
raise PaymentFailure ( f ' Not enough local balance. Have: { self . available_to_spend ( LOCAL ) } , Need: { amount_msat } ' )
if len ( self . hm . htlcs ( LOCAL ) ) + 1 > self . config [ REMOTE ] . max_accepted_htlcs :
raise PaymentFailure ( ' Too many HTLCs already in channel ' )
current_htlc_sum = htlcsum ( self . hm . htlcs_by_direction ( LOCAL , SENT ) ) + htlcsum ( self . hm . htlcs_by_direction ( LOCAL , RECEIVED ) )
if current_htlc_sum + amount_msat > self . config [ REMOTE ] . max_htlc_value_in_flight_msat :
raise PaymentFailure ( f ' HTLC value sum (sum of pending htlcs: { current_htlc_sum / 1000 } sat plus new htlc: { amount_msat / 1000 } sat) would exceed max allowed: { self . config [ REMOTE ] . max_htlc_value_in_flight_msat / 1000 } sat ' )
if amount_msat < self . config [ REMOTE ] . htlc_minimum_msat :
raise PaymentFailure ( f ' HTLC value too small: { amount_msat } msat ' )
def can_pay ( self , amount_msat ) :
try :
self . _check_can_pay ( amount_msat )
except PaymentFailure :
return False
return True
def set_funding_txo_spentness ( self , is_spent : bool ) :
assert isinstance ( is_spent , bool )
self . _is_funding_txo_spent = is_spent
def should_try_to_reestablish_peer ( self ) - > bool :
return self . _is_funding_txo_spent is False and self . _state == ' DISCONNECTED '
def get_funding_address ( self ) :
script = funding_output_script ( self . config [ LOCAL ] , self . config [ REMOTE ] )
return redeem_script_to_address ( ' p2wsh ' , script )
def add_htlc ( self , htlc : UpdateAddHtlc ) - > UpdateAddHtlc :
"""
AddHTLC adds an HTLC to the state machine ' s local update log. This method
should be called when preparing to send an outgoing HTLC .
This docstring is from LND .
"""
if isinstance ( htlc , dict ) : # legacy conversion # FIXME remove
htlc = UpdateAddHtlc ( * * htlc )
assert isinstance ( htlc , UpdateAddHtlc )
self . _check_can_pay ( htlc . amount_msat )
htlc = htlc . _replace ( htlc_id = self . config [ LOCAL ] . next_htlc_id )
self . hm . send_htlc ( htlc )
self . print_error ( " add_htlc " )
self . config [ LOCAL ] = self . config [ LOCAL ] . _replace ( next_htlc_id = htlc . htlc_id + 1 )
return htlc
def receive_htlc ( self , htlc : UpdateAddHtlc ) - > UpdateAddHtlc :
"""
ReceiveHTLC adds an HTLC to the state machine ' s remote update log. This
method should be called in response to receiving a new HTLC from the remote
party .
This docstring is from LND .
"""
if isinstance ( htlc , dict ) : # legacy conversion # FIXME remove
htlc = UpdateAddHtlc ( * * htlc )
assert isinstance ( htlc , UpdateAddHtlc )
htlc = htlc . _replace ( htlc_id = self . config [ REMOTE ] . next_htlc_id )
if 0 < = self . available_to_spend ( REMOTE ) < htlc . amount_msat :
raise RemoteMisbehaving ( ' Remote dipped below channel reserve. ' + \
f ' Available at remote: { self . available_to_spend ( REMOTE ) } , ' + \
f ' HTLC amount: { htlc . amount_msat } ' )
self . hm . recv_htlc ( htlc )
self . print_error ( " receive_htlc " )
self . config [ REMOTE ] = self . config [ REMOTE ] . _replace ( next_htlc_id = htlc . htlc_id + 1 )
return htlc
def sign_next_commitment ( self ) :
"""
SignNextCommitment signs a new commitment which includes any previous
unsettled HTLCs , any new HTLCs , and any modifications to prior HTLCs
committed in previous commitment updates .
The first return parameter is the signature for the commitment transaction
itself , while the second parameter is are all HTLC signatures concatenated .
any ) . The HTLC signatures are sorted according to the BIP 69 order of the
HTLC ' s on the commitment transaction.
This docstring was adapted from LND .
"""
next_remote_ctn = self . get_current_ctn ( REMOTE ) + 1
self . print_error ( " sign_next_commitment " , next_remote_ctn )
self . hm . send_ctx ( )
pending_remote_commitment = self . pending_commitment ( REMOTE )
sig_64 = sign_and_get_sig_string ( pending_remote_commitment , self . config [ LOCAL ] , self . config [ REMOTE ] )
their_remote_htlc_privkey_number = derive_privkey (
int . from_bytes ( self . config [ LOCAL ] . htlc_basepoint . privkey , ' big ' ) ,
self . config [ REMOTE ] . next_per_commitment_point )
their_remote_htlc_privkey = their_remote_htlc_privkey_number . to_bytes ( 32 , ' big ' )
for_us = False
htlcsigs = [ ]
# they sent => we receive
for we_receive , htlcs in zip ( [ True , False ] , [ self . included_htlcs ( REMOTE , SENT , ctn = next_remote_ctn ) ,
self . included_htlcs ( REMOTE , RECEIVED , ctn = next_remote_ctn ) ] ) :
for htlc in htlcs :
_script , htlc_tx = make_htlc_tx_with_open_channel ( chan = self ,
pcp = self . config [ REMOTE ] . next_per_commitment_point ,
for_us = for_us ,
we_receive = we_receive ,
commit = pending_remote_commitment ,
htlc = htlc )
sig = bfh ( htlc_tx . sign_txin ( 0 , their_remote_htlc_privkey ) )
htlc_sig = ecc . sig_string_from_der_sig ( sig [ : - 1 ] )
htlc_output_idx = htlc_tx . inputs ( ) [ 0 ] [ ' prevout_n ' ]
htlcsigs . append ( ( htlc_output_idx , htlc_sig ) )
htlcsigs . sort ( )
htlcsigs = [ x [ 1 ] for x in htlcsigs ]
# TODO should add remote_commitment here and handle
# both valid ctx'es in lnwatcher at the same time...
return sig_64 , htlcsigs
def receive_new_commitment ( self , sig , htlc_sigs ) :
"""
ReceiveNewCommitment process a signature for a new commitment state sent by
the remote party . This method should be called in response to the
remote party initiating a new change , or when the remote party sends a
signature fully accepting a new state we ' ve initiated. If we are able to
successfully validate the signature , then the generated commitment is added
to our local commitment chain . Once we send a revocation for our prior
state , then this newly added commitment becomes our current accepted channel
state .
This docstring is from LND .
"""
self . print_error ( " receive_new_commitment " )
self . hm . recv_ctx ( )
assert len ( htlc_sigs ) == 0 or type ( htlc_sigs [ 0 ] ) is bytes
pending_local_commitment = self . pending_commitment ( LOCAL )
preimage_hex = pending_local_commitment . serialize_preimage ( 0 )
pre_hash = sha256d ( bfh ( preimage_hex ) )
if not ecc . verify_signature ( self . config [ REMOTE ] . multisig_key . pubkey , sig , pre_hash ) :
raise Exception ( ' failed verifying signature of our updated commitment transaction: ' + bh2u ( sig ) + ' preimage is ' + preimage_hex )
htlc_sigs_string = b ' ' . join ( htlc_sigs )
htlc_sigs = htlc_sigs [ : ] # copy cause we will delete now
next_local_ctn = self . get_current_ctn ( LOCAL ) + 1
for htlcs , we_receive in [ ( self . included_htlcs ( LOCAL , SENT , ctn = next_local_ctn ) , False ) ,
( self . included_htlcs ( LOCAL , RECEIVED , ctn = next_local_ctn ) , True ) ] :
for htlc in htlcs :
idx = self . verify_htlc ( htlc , htlc_sigs , we_receive , pending_local_commitment )
del htlc_sigs [ idx ]
if len ( htlc_sigs ) != 0 : # all sigs should have been popped above
raise Exception ( ' failed verifying HTLC signatures: invalid amount of correct signatures ' )
self . config [ LOCAL ] = self . config [ LOCAL ] . _replace (
current_commitment_signature = sig ,
current_htlc_signatures = htlc_sigs_string ,
got_sig_for_next = True )
if self . pending_fee is not None :
if not self . constraints . is_initiator :
self . pending_fee [ FUNDEE_SIGNED ] = True
if self . constraints . is_initiator and self . pending_fee [ FUNDEE_ACKED ] :
self . pending_fee [ FUNDER_SIGNED ] = True
self . set_local_commitment ( pending_local_commitment )
def verify_htlc ( self , htlc : UpdateAddHtlc , htlc_sigs : Sequence [ bytes ] , we_receive : bool , ctx ) - > int :
ctn = extract_ctn_from_tx_and_chan ( ctx , self )
secret = get_per_commitment_secret_from_seed ( self . config [ LOCAL ] . per_commitment_secret_seed , RevocationStore . START_INDEX - ctn )
point = secret_to_pubkey ( int . from_bytes ( secret , ' big ' ) )
_script , htlc_tx = make_htlc_tx_with_open_channel ( chan = self ,
pcp = point ,
for_us = True ,
we_receive = we_receive ,
commit = ctx ,
htlc = htlc )
pre_hash = sha256d ( bfh ( htlc_tx . serialize_preimage ( 0 ) ) )
remote_htlc_pubkey = derive_pubkey ( self . config [ REMOTE ] . htlc_basepoint . pubkey , point )
for idx , sig in enumerate ( htlc_sigs ) :
if ecc . verify_signature ( remote_htlc_pubkey , sig , pre_hash ) :
return idx
else :
raise Exception ( f ' failed verifying HTLC signatures: { htlc } , sigs: { len ( htlc_sigs ) } , we_receive: { we_receive } ' )
def get_remote_htlc_sig_for_htlc ( self , htlc : UpdateAddHtlc , we_receive : bool , ctx ) - > bytes :
data = self . config [ LOCAL ] . current_htlc_signatures
htlc_sigs = [ data [ i : i + 64 ] for i in range ( 0 , len ( data ) , 64 ) ]
idx = self . verify_htlc ( htlc , htlc_sigs , we_receive = we_receive , ctx = ctx )
remote_htlc_sig = ecc . der_sig_from_sig_string ( htlc_sigs [ idx ] ) + b ' \x01 '
return remote_htlc_sig
def revoke_current_commitment ( self ) :
self . print_error ( " revoke_current_commitment " )
last_secret , this_point , next_point , _ = self . points ( )
new_feerate = self . constraints . feerate
if self . pending_fee is not None :
if not self . constraints . is_initiator and self . pending_fee [ FUNDEE_SIGNED ] :
new_feerate = self . pending_fee . rate
self . pending_fee = None
print ( " FEERATE CHANGE COMPLETE (non-initiator) " )
if self . constraints . is_initiator and self . pending_fee [ FUNDER_SIGNED ] :
new_feerate = self . pending_fee . rate
self . pending_fee = None
print ( " FEERATE CHANGE COMPLETE (initiator) " )
assert self . config [ LOCAL ] . got_sig_for_next
self . constraints = self . constraints . _replace (
feerate = new_feerate
)
self . set_local_commitment ( self . pending_commitment ( LOCAL ) )
ctx = self . pending_commitment ( LOCAL )
self . hm . send_rev ( )
self . config [ LOCAL ] = self . config [ LOCAL ] . _replace (
ctn = self . config [ LOCAL ] . ctn + 1 ,
got_sig_for_next = False ,
)
assert self . signature_fits ( ctx )
return RevokeAndAck ( last_secret , next_point ) , " current htlcs "
def points ( self ) :
last_small_num = self . config [ LOCAL ] . ctn
this_small_num = last_small_num + 1
next_small_num = last_small_num + 2
last_secret = get_per_commitment_secret_from_seed ( self . config [ LOCAL ] . per_commitment_secret_seed , RevocationStore . START_INDEX - last_small_num )
this_secret = get_per_commitment_secret_from_seed ( self . config [ LOCAL ] . per_commitment_secret_seed , RevocationStore . START_INDEX - this_small_num )
this_point = secret_to_pubkey ( int . from_bytes ( this_secret , ' big ' ) )
next_secret = get_per_commitment_secret_from_seed ( self . config [ LOCAL ] . per_commitment_secret_seed , RevocationStore . START_INDEX - next_small_num )
next_point = secret_to_pubkey ( int . from_bytes ( next_secret , ' big ' ) )
last_point = secret_to_pubkey ( int . from_bytes ( last_secret , ' big ' ) )
return last_secret , this_point , next_point , last_point
def process_new_revocation_secret ( self , per_commitment_secret : bytes ) :
if not self . lnwatcher :
return
outpoint = self . funding_outpoint . to_str ( )
ctx = self . remote_commitment_to_be_revoked # FIXME can't we just reconstruct it?
sweeptxs = create_sweeptxs_for_their_just_revoked_ctx ( self , ctx , per_commitment_secret , self . sweep_address )
for prev_txid , tx in sweeptxs . items ( ) :
if tx is not None :
self . lnwatcher . add_sweep_tx ( outpoint , prev_txid , tx . as_dict ( ) )
def receive_revocation ( self , revocation ) - > Tuple [ int , int ] :
self . print_error ( " receive_revocation " )
cur_point = self . config [ REMOTE ] . current_per_commitment_point
derived_point = ecc . ECPrivkey ( revocation . per_commitment_secret ) . get_public_key_bytes ( compressed = True )
if cur_point != derived_point :
raise Exception ( ' revoked secret not for current point ' )
# FIXME not sure this is correct... but it seems to work
# if there are update_add_htlc msgs between commitment_signed and rev_ack,
# this might break
prev_remote_commitment = self . pending_commitment ( REMOTE )
self . config [ REMOTE ] . revocation_store . add_next_entry ( revocation . per_commitment_secret )
self . process_new_revocation_secret ( revocation . per_commitment_secret )
##### start applying fee/htlc changes
if self . pending_fee is not None :
if not self . constraints . is_initiator :
self . pending_fee [ FUNDEE_SIGNED ] = True
if self . constraints . is_initiator and self . pending_fee [ FUNDEE_ACKED ] :
self . pending_fee [ FUNDER_SIGNED ] = True
received = self . hm . received_in_ctn ( self . config [ REMOTE ] . ctn + 1 )
sent = self . hm . sent_in_ctn ( self . config [ REMOTE ] . ctn + 1 )
for htlc in received :
self . payment_completed ( self , RECEIVED , htlc , None )
for htlc in sent :
preimage = self . preimages . pop ( htlc . htlc_id )
self . payment_completed ( self , SENT , htlc , preimage )
received_this_batch = htlcsum ( received )
sent_this_batch = htlcsum ( sent )
next_point = self . config [ REMOTE ] . next_per_commitment_point
self . hm . recv_rev ( )
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 ,
)
if self . pending_fee is not None :
if self . constraints . is_initiator :
self . pending_fee [ FUNDEE_ACKED ] = True
self . set_remote_commitment ( )
self . remote_commitment_to_be_revoked = prev_remote_commitment
return received_this_batch , sent_this_batch
def balance ( self , subject , ctn = None ) :
"""
This balance in mSAT is not including reserve and fees .
So a node cannot actually use it ' s whole balance.
But this number is simple , since it is derived simply
from the initial balance , and the value of settled HTLCs .
Note that it does not decrease once an HTLC is added ,
failed or fulfilled , since the balance change is only
commited to later when the respective commitment
transaction as been revoked .
"""
assert type ( subject ) is HTLCOwner
initial = self . config [ subject ] . initial_msat
for direction , htlc in self . hm . settled_htlcs ( subject , ctn ) :
if direction == SENT :
initial - = htlc . amount_msat
else :
initial + = htlc . amount_msat
return initial
def balance_minus_outgoing_htlcs ( self , subject ) :
"""
This balance in mSAT , which includes the value of
pending outgoing HTLCs , is used in the UI .
"""
assert type ( subject ) is HTLCOwner
ctn = self . hm . log [ subject ] [ ' ctn ' ] + 1
return self . balance ( subject , ctn ) \
- htlcsum ( self . hm . htlcs_by_direction ( subject , SENT , ctn ) )
def available_to_spend ( self , subject ) :
"""
This balance in mSAT , while technically correct , can
not be used in the UI cause it fluctuates ( commit fee )
"""
assert type ( subject ) is HTLCOwner
return self . balance_minus_outgoing_htlcs ( subject ) \
- self . config [ - subject ] . reserve_sat * 1000 \
- calc_onchain_fees (
# TODO should we include a potential new htlc, when we are called from receive_htlc?
len ( self . included_htlcs ( subject , SENT ) + self . included_htlcs ( subject , RECEIVED ) ) ,
self . pending_feerate ( subject ) ,
self . constraints . is_initiator ,
) [ subject ]
def included_htlcs ( self , subject , direction , ctn = None ) :
"""
return filter of non - dust htlcs for subjects commitment transaction , initiated by given party
"""
assert type ( subject ) is HTLCOwner
assert type ( direction ) is Direction
if ctn is None :
ctn = self . config [ subject ] . ctn
feerate = self . pending_feerate ( subject )
conf = self . config [ subject ]
if ( subject , direction ) in [ ( REMOTE , RECEIVED ) , ( LOCAL , SENT ) ] :
weight = HTLC_SUCCESS_WEIGHT
else :
weight = HTLC_TIMEOUT_WEIGHT
htlcs = self . hm . htlcs_by_direction ( subject , direction , ctn = ctn )
lnhtlc: multiply weight by feerate before rounding
This resolves the error formerly manifested as:
Traceback (most recent call last):
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/packages/jsonrpclib/SimpleJSONRPCServer.py", line 376, in _dispatch
return func(*params)
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/daemon.py", line 292, in run_cmdline
result = func(*args, **kwargs)
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/commands.py", line 87, in func_wrapper
return func(*args, **kwargs)
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/commands.py", line 697, in lnpay
return f.result()
File "/usr/lib/python3.6/concurrent/futures/_base.py", line 432, in result
return self.__get_result()
File "/usr/lib/python3.6/concurrent/futures/_base.py", line 384, in __get_result
raise self._exception
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/lnbase.py", line 887, in pay
sig_64, htlc_sigs = chan.sign_next_commitment()
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/lnhtlc.py", line 281, in sign_next_commitment
htlc_tx = make_htlc_tx_with_open_channel(self, *args)
File "/home/janus/Skrivebord/lightning-rfc/tools/electrum/electrum/lnutil.py", line 262, in make_htlc_tx_with_open_channel
commit.txid(), commit.htlc_output_indices[original_htlc_output_index],
KeyError: 0
7 years ago
fee_for_htlc = lambda htlc : htlc . amount_msat / / 1000 - ( weight * feerate / / 1000 )
return list ( filter ( lambda htlc : fee_for_htlc ( htlc ) > = conf . dust_limit_sat , htlcs ) )
def pending_feerate ( self , subject ) :
assert type ( subject ) is HTLCOwner
candidate = self . constraints . feerate
if self . pending_fee is not None :
x = self . pending_fee . pending_feerate ( subject )
if x is not None :
candidate = x
return candidate
def pending_commitment ( self , subject ) :
assert type ( subject ) is HTLCOwner
this_point = self . config [ REMOTE ] . next_per_commitment_point if subject == REMOTE else self . points ( ) [ 1 ]
ctn = self . config [ subject ] . ctn + 1
feerate = self . pending_feerate ( subject )
return self . make_commitment ( subject , this_point , ctn , feerate , True )
def current_commitment ( self , subject ) :
assert type ( subject ) is HTLCOwner
this_point = self . config [ REMOTE ] . current_per_commitment_point if subject == REMOTE else self . points ( ) [ 3 ]
ctn = self . config [ subject ] . ctn
feerate = self . constraints . feerate
return self . make_commitment ( subject , this_point , ctn , feerate , False )
def get_current_ctn ( self , subject ) :
return self . config [ subject ] . ctn
def total_msat ( self , direction ) :
assert type ( direction ) is Direction
sub = LOCAL if direction == SENT else REMOTE
return htlcsum ( self . hm . settled_htlcs_by ( sub , self . config [ sub ] . ctn ) )
def settle_htlc ( self , preimage , htlc_id ) :
"""
SettleHTLC attempts to settle an existing outstanding received HTLC .
"""
self . print_error ( " settle_htlc " )
log = self . hm . log [ REMOTE ]
htlc = log [ ' adds ' ] [ htlc_id ]
assert htlc . payment_hash == sha256 ( preimage )
assert htlc_id not in log [ ' settles ' ]
self . hm . send_settle ( htlc_id )
# not saving preimage because it's already saved in LNWorker.invoices
def receive_htlc_settle ( self , preimage , htlc_id ) :
self . print_error ( " receive_htlc_settle " )
log = self . hm . log [ LOCAL ]
htlc = log [ ' adds ' ] [ htlc_id ]
assert htlc . payment_hash == sha256 ( preimage )
assert htlc_id not in log [ ' settles ' ]
self . hm . recv_settle ( htlc_id )
self . preimages [ htlc_id ] = preimage
# we don't save the preimage because we don't need to forward it anyway
def fail_htlc ( self , htlc_id ) :
self . print_error ( " fail_htlc " )
self . hm . send_fail ( htlc_id )
def receive_fail_htlc ( self , htlc_id ) :
self . print_error ( " receive_fail_htlc " )
self . hm . recv_fail ( htlc_id )
@property
def current_height ( self ) :
return { LOCAL : self . config [ LOCAL ] . ctn , REMOTE : self . config [ REMOTE ] . ctn }
def pending_local_fee ( self ) :
return self . constraints . capacity - sum ( x [ 2 ] for x in self . pending_commitment ( LOCAL ) . outputs ( ) )
def update_fee ( self , feerate , initiator ) :
if self . constraints . is_initiator != initiator :
raise Exception ( " Cannot update_fee: wrong initiator " , initiator )
if self . pending_fee is not None :
raise Exception ( " a fee update is already in progress " )
self . pending_fee = FeeUpdate ( self , rate = feerate )
def to_save ( self ) :
to_save = {
" local_config " : self . config [ LOCAL ] ,
" remote_config " : self . config [ REMOTE ] ,
" channel_id " : self . channel_id ,
" short_channel_id " : self . short_channel_id ,
" constraints " : self . constraints ,
" funding_outpoint " : self . funding_outpoint ,
" node_id " : self . node_id ,
" remote_commitment_to_be_revoked " : str ( self . remote_commitment_to_be_revoked ) ,
" log " : self . hm . to_save ( ) ,
" onion_keys " : str_bytes_dict_to_save ( self . onion_keys ) ,
" force_closed " : self . get_state ( ) == ' FORCE_CLOSING ' ,
}
return to_save
def serialize ( self ) :
namedtuples_to_dict = lambda v : { i : j . _asdict ( ) if isinstance ( j , tuple ) else j for i , j in v . _asdict ( ) . items ( ) }
serialized_channel = { }
to_save_ref = self . to_save ( )
for k , v in to_save_ref . items ( ) :
if isinstance ( v , tuple ) :
serialized_channel [ k ] = namedtuples_to_dict ( v )
else :
serialized_channel [ k ] = v
dumped = ChannelJsonEncoder ( ) . encode ( serialized_channel )
roundtripped = json . loads ( dumped )
reconstructed = Channel ( roundtripped )
to_save_new = reconstructed . to_save ( )
if to_save_new != to_save_ref :
from pprint import PrettyPrinter
pp = PrettyPrinter ( indent = 168 )
try :
from deepdiff import DeepDiff
except ImportError :
raise Exception ( " Channels did not roundtrip serialization without changes: \n " + pp . pformat ( to_save_ref ) + " \n " + pp . pformat ( to_save_new ) )
else :
raise Exception ( " Channels did not roundtrip serialization without changes: \n " + pp . pformat ( DeepDiff ( to_save_ref , to_save_new ) ) )
return roundtripped
def __str__ ( self ) :
return str ( self . serialize ( ) )
def make_commitment ( self , subject , this_point , ctn , feerate , pending ) - > Transaction :
#if subject == REMOTE and not pending:
# ctn -= 1
assert type ( subject ) is HTLCOwner
other = REMOTE if LOCAL == subject else LOCAL
remote_msat , local_msat = self . balance ( other , ctn ) , self . balance ( subject , ctn )
received_htlcs = self . hm . htlcs_by_direction ( subject , SENT if subject == LOCAL else RECEIVED , ctn )
sent_htlcs = self . hm . htlcs_by_direction ( subject , RECEIVED if subject == LOCAL else SENT , ctn )
if subject != LOCAL :
remote_msat - = htlcsum ( received_htlcs )
local_msat - = htlcsum ( sent_htlcs )
else :
remote_msat - = htlcsum ( sent_htlcs )
local_msat - = htlcsum ( received_htlcs )
assert remote_msat > = 0
assert local_msat > = 0
# same htlcs as before, but now without dust.
received_htlcs = self . included_htlcs ( subject , SENT if subject == LOCAL else RECEIVED , ctn )
sent_htlcs = self . included_htlcs ( subject , RECEIVED if subject == LOCAL else SENT , ctn )
this_config = self . config [ subject ]
other_config = self . config [ - subject ]
other_htlc_pubkey = derive_pubkey ( other_config . htlc_basepoint . pubkey , this_point )
this_htlc_pubkey = derive_pubkey ( this_config . htlc_basepoint . pubkey , this_point )
other_revocation_pubkey = derive_blinded_pubkey ( other_config . revocation_basepoint . pubkey , this_point )
htlcs = [ ] # type: List[ScriptHtlc]
for is_received_htlc , htlc_list in zip ( ( subject != LOCAL , subject == LOCAL ) , ( received_htlcs , sent_htlcs ) ) :
for htlc in htlc_list :
htlcs . append ( ScriptHtlc ( make_htlc_output_witness_script (
is_received_htlc = is_received_htlc ,
remote_revocation_pubkey = other_revocation_pubkey ,
remote_htlc_pubkey = other_htlc_pubkey ,
local_htlc_pubkey = this_htlc_pubkey ,
payment_hash = htlc . payment_hash ,
cltv_expiry = htlc . cltv_expiry ) , htlc ) )
onchain_fees = calc_onchain_fees (
len ( htlcs ) ,
feerate ,
self . constraints . is_initiator == ( subject == LOCAL ) ,
)
payment_pubkey = derive_pubkey ( other_config . payment_basepoint . pubkey , this_point )
return make_commitment (
ctn ,
this_config . multisig_key . pubkey ,
other_config . multisig_key . pubkey ,
payment_pubkey ,
self . config [ LOCAL if self . constraints . is_initiator else REMOTE ] . payment_basepoint . pubkey ,
self . config [ LOCAL if not self . constraints . is_initiator else REMOTE ] . payment_basepoint . pubkey ,
other_revocation_pubkey ,
derive_pubkey ( this_config . delayed_basepoint . pubkey , this_point ) ,
other_config . to_self_delay ,
* self . funding_outpoint ,
self . constraints . capacity ,
local_msat ,
remote_msat ,
this_config . dust_limit_sat ,
onchain_fees ,
htlcs = htlcs )
def get_local_index ( self ) :
return int ( self . config [ LOCAL ] . multisig_key . pubkey < self . config [ REMOTE ] . multisig_key . pubkey )
def make_closing_tx ( self , local_script : bytes , remote_script : bytes ,
fee_sat : int ) - > Tuple [ bytes , int , str ] :
""" cooperative close """
_ , outputs = make_commitment_outputs ( {
LOCAL : fee_sat * 1000 if self . constraints . is_initiator else 0 ,
REMOTE : fee_sat * 1000 if not self . constraints . is_initiator else 0 ,
} ,
self . balance ( LOCAL ) ,
self . balance ( REMOTE ) ,
( TYPE_SCRIPT , bh2u ( local_script ) ) ,
( TYPE_SCRIPT , bh2u ( remote_script ) ) ,
[ ] , self . config [ LOCAL ] . dust_limit_sat )
closing_tx = make_closing_tx ( self . config [ LOCAL ] . multisig_key . pubkey ,
self . config [ REMOTE ] . multisig_key . pubkey ,
funding_txid = self . funding_outpoint . txid ,
funding_pos = self . funding_outpoint . output_index ,
funding_sat = self . constraints . capacity ,
outputs = outputs )
der_sig = bfh ( closing_tx . sign_txin ( 0 , self . config [ LOCAL ] . multisig_key . privkey ) )
sig = ecc . sig_string_from_der_sig ( der_sig [ : - 1 ] )
return sig , closing_tx
def signature_fits ( self , tx ) :
remote_sig = self . config [ LOCAL ] . current_commitment_signature
preimage_hex = tx . serialize_preimage ( 0 )
pre_hash = sha256d ( bfh ( preimage_hex ) )
assert remote_sig
res = ecc . verify_signature ( self . config [ REMOTE ] . multisig_key . pubkey , remote_sig , pre_hash )
return res
def force_close_tx ( self ) :
tx = self . local_commitment
assert self . signature_fits ( tx )
tx = Transaction ( str ( tx ) )
tx . deserialize ( True )
tx . sign ( { bh2u ( self . config [ LOCAL ] . multisig_key . pubkey ) : ( self . config [ LOCAL ] . multisig_key . privkey , True ) } )
remote_sig = self . config [ LOCAL ] . current_commitment_signature
remote_sig = ecc . der_sig_from_sig_string ( remote_sig ) + b " \x01 "
sigs = tx . _inputs [ 0 ] [ " signatures " ]
none_idx = sigs . index ( None )
tx . add_signature_to_txin ( 0 , none_idx , bh2u ( remote_sig ) )
assert tx . is_complete ( )
return tx
def included_htlcs_in_their_latest_ctxs ( self , htlc_initiator ) - > Dict [ int , List [ UpdateAddHtlc ] ] :
""" A map from commitment number to list of HTLCs in
their latest two commitment transactions .
The oldest might have been revoked . """
assert type ( htlc_initiator ) is HTLCOwner
direction = RECEIVED if htlc_initiator == LOCAL else SENT
old_ctn = self . config [ REMOTE ] . ctn
old_htlcs = self . included_htlcs ( REMOTE , direction , ctn = old_ctn )
new_ctn = self . config [ REMOTE ] . ctn + 1
new_htlcs = self . included_htlcs ( REMOTE , direction , ctn = new_ctn )
return { old_ctn : old_htlcs ,
new_ctn : new_htlcs , }