@ -1,3 +1,5 @@
import threading
from . util import PrintError , bh2u , bfh , NoDynamicFeeEstimates
from . lnutil import ( funding_output_script , extract_ctn_from_tx , derive_privkey ,
get_per_commitment_secret_from_seed , derive_pubkey ,
@ -8,6 +10,10 @@ from .bitcoin import redeem_script_to_address, TYPE_ADDRESS
from . import transaction
from . transaction import Transaction
from . import ecc
from . import wallet
TX_MINED_STATUS_DEEP , TX_MINED_STATUS_SHALLOW , TX_MINED_STATUS_MEMPOOL , TX_MINED_STATUS_FREE = range ( 0 , 4 )
class LNWatcher ( PrintError ) :
@ -51,13 +57,32 @@ def funding_address_for_channel(chan):
class LNChanCloseHandler ( PrintError ) :
# TODO if verifier gets an incorrect merkle proof, that tx will never verify!!
# similarly, what if server ignores request for merkle proof?
# maybe we should disconnect from server in these cases
def __init__ ( self , network , wallet , chan ) :
self . network = network
self . wallet = wallet
self . chan = chan
self . lock = threading . Lock ( )
self . funding_address = funding_address_for_channel ( chan )
network . request_address_history ( self . funding_address , self . on_history )
self . watched_addresses = set ( )
network . register_callback ( self . on_network_update , [ ' updated ' ] )
self . watch_address ( self . funding_address )
def on_network_update ( self , event , * args ) :
if self . wallet . synchronizer . is_up_to_date ( ) :
self . check_onchain_situation ( )
def stop_and_delete ( self ) :
self . network . unregister_callback ( self . on_network_update )
# TODO delete channel from wallet storage?
def watch_address ( self , addr ) :
with self . lock :
self . watched_addresses . add ( addr )
self . wallet . synchronizer . add ( addr )
# TODO: de-duplicate?
def parse_response ( self , response ) :
@ -66,46 +91,38 @@ class LNChanCloseHandler(PrintError):
return None , None
return response [ ' params ' ] , response [ ' result ' ]
def on_history ( self , response ) :
params , result = self . parse_response ( response )
if not params :
return
addr = params [ 0 ]
if self . funding_address != addr :
self . print_error ( " unexpected funding address: {} != {} "
. format ( self . funding_address , addr ) )
return
txids = set ( map ( lambda item : item [ ' tx_hash ' ] , result ) )
self . network . get_transactions ( txids , self . on_tx_response )
def on_tx_response ( self , response ) :
params , result = self . parse_response ( response )
if not params :
return
tx_hash = params [ 0 ]
tx = Transaction ( result )
try :
tx . deserialize ( )
except Exception :
self . print_msg ( " cannot deserialize transaction " , tx_hash )
def check_onchain_situation ( self ) :
funding_outpoint = self . chan . funding_outpoint
ctx_candidate_txid = self . wallet . spent_outpoints [ funding_outpoint . txid ] . get ( funding_outpoint . output_index )
if ctx_candidate_txid is None :
return
if tx_hash != tx . txid ( ) :
self . print_error ( " received tx does not match expected txid ( {} != {} ) "
. format ( tx_hash , tx . txid ( ) ) )
ctx_candidate = self . wallet . transactions . get ( ctx_candidate_txid )
if ctx_candidate is None :
return
funding_outpoint = self . chan . funding_outpoint
for i , txin in enumerate ( tx . inputs ( ) ) :
if txin [ ' prevout_hash ' ] == funding_outpoint . txid \
and txin [ ' pr evout_n ' ] == funding_outpoint . output_index :
self . print_error ( " funding outpoint {} is spent by {} "
. format ( funding_outpoint , tx_hash ) )
self . inspect_spending_tx ( tx , i )
#self.print_error("funding outpoint {} is spent by {}"
# .format(funding_outpoint, ctx_candidate_txid))
for i , txin in enumerate ( ctx_candidate . inputs ( ) ) :
if txin [ ' type ' ] == ' coinbase ' : continue
prevout_hash = txin [ ' prevout_hash ' ]
prevout_n = txin [ ' prevout_n ' ]
if prevout_hash == funding_outpoint . txid and prevout_n == funding_outpoint . output_index :
break
else :
raise Exception ( ' {} is supposed to be spent by {} , but none of the inputs spend it '
. format ( funding_outpoint , ctx_candidate_txid ) )
height , conf , timestamp = self . wallet . get_tx_height ( ctx_candidate_txid )
if conf == 0 :
return
keep_watching_this = self . inspect_ctx_candidate ( ctx_candidate , i )
if not keep_watching_this :
self . stop_and_delete ( )
# TODO batch sweeps
# TODO sweep HTLC outputs
# TODO implement nursery that waits for timelocks
def inspect_spending_tx ( self , ctx , txin_idx : int ) :
def inspect_ctx_candidate ( self , ctx , txin_idx : int ) :
""" Returns True iff found any not-deeply-spent outputs that we could
potentially sweep at some point . """
keep_watching_this = False
chan = self . chan
ctn = extract_ctn_from_tx ( ctx , txin_idx ,
chan . local_config . payment_basepoint . pubkey ,
@ -119,7 +136,7 @@ class LNChanCloseHandler(PrintError):
# note that we might also get here if this is our ctx and the ctn just happens to match
their_cur_pcp = chan . remote_state . current_per_commitment_point
if their_cur_pcp is not None :
self . find_and_sweep_their_ctx_to_remote ( ctx , their_cur_pcp )
keep_watching_this | = self . find_and_sweep_their_ctx_to_remote ( ctx , their_cur_pcp )
# see if we have a revoked secret for this ctn ("breach")
try :
per_commitment_secret = chan . remote_state . revocation_store . retrieve_secret (
@ -130,15 +147,36 @@ class LNChanCloseHandler(PrintError):
# note that we might also get here if this is our ctx and we just happen to have
# the secret for the symmetric ctn
their_pcp = ecc . ECPrivkey ( per_commitment_secret ) . get_public_key_bytes ( compressed = True )
self . find_and_sweep_their_ctx_to_remote ( ctx , their_pcp )
self . find_and_sweep_their_ctx_to_local ( ctx , per_commitment_secret )
keep_watching_this | = self . find_and_sweep_their_ctx_to_remote ( ctx , their_pcp )
keep_watching_this | = self . find_and_sweep_their_ctx_to_local ( ctx , per_commitment_secret )
# see if it's our ctx
our_per_commitment_secret = get_per_commitment_secret_from_seed (
chan . local_state . per_commitment_secret_seed , RevocationStore . START_INDEX - ctn )
our_per_commitment_point = ecc . ECPrivkey ( our_per_commitment_secret ) . get_public_key_bytes ( compressed = True )
self . find_and_sweep_our_ctx_to_local ( ctx , our_per_commitment_point )
keep_watching_this | = self . find_and_sweep_our_ctx_to_local ( ctx , our_per_commitment_point )
return keep_watching_this
def get_tx_mined_status ( self , txid ) :
if not txid :
return TX_MINED_STATUS_FREE
height , conf , timestamp = self . wallet . get_tx_height ( txid )
if conf > 100 :
return TX_MINED_STATUS_DEEP
elif conf > 0 :
return TX_MINED_STATUS_SHALLOW
elif height in ( wallet . TX_HEIGHT_UNCONFIRMED , wallet . TX_HEIGHT_UNCONF_PARENT ) :
return TX_MINED_STATUS_MEMPOOL
elif height == wallet . TX_HEIGHT_LOCAL :
return TX_MINED_STATUS_FREE
elif height > 0 and conf == 0 :
# unverified but claimed to be mined
return TX_MINED_STATUS_MEMPOOL
else :
raise NotImplementedError ( )
def find_and_sweep_their_ctx_to_remote ( self , ctx , their_pcp : bytes ) :
""" Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point . """
payment_bp_privkey = ecc . ECPrivkey ( self . chan . local_config . payment_basepoint . privkey )
our_payment_privkey = derive_privkey ( payment_bp_privkey . secret_scalar , their_pcp )
our_payment_privkey = ecc . ECPrivkey . from_secret_scalar ( our_payment_privkey )
@ -151,40 +189,26 @@ class LNChanCloseHandler(PrintError):
#self.print_error("ctx {} is normal unilateral close by them".format(ctx.txid()))
break
else :
return
sweep_tx = self . create_sweeptx_their_ctx_to_remote ( ctx , output_idx , our_payment_privkey )
return False
if to_remote_address not in self . watched_addresses :
self . watch_address ( to_remote_address )
return True
spending_txid = self . wallet . spent_outpoints [ ctx . txid ( ) ] . get ( output_idx )
stx_mined_status = self . get_tx_mined_status ( spending_txid )
if stx_mined_status == TX_MINED_STATUS_DEEP :
return False
elif stx_mined_status in ( TX_MINED_STATUS_SHALLOW , TX_MINED_STATUS_MEMPOOL ) :
return True
sweep_tx = create_sweeptx_their_ctx_to_remote ( self . network , self . wallet , ctx ,
output_idx , our_payment_privkey )
self . network . broadcast_transaction ( sweep_tx ,
lambda res : self . print_tx_broadcast_result ( ' sweep_their_ctx_to_remote ' , res ) )
return True
def create_sweeptx_their_ctx_to_remote ( self , ctx , output_idx : int , our_payment_privkey : ecc . ECPrivkey ) :
our_payment_pubkey = our_payment_privkey . get_public_key_hex ( compressed = True )
val = ctx . outputs ( ) [ output_idx ] [ 2 ]
sweep_inputs = [ {
' type ' : ' p2wpkh ' ,
' x_pubkeys ' : [ our_payment_pubkey ] ,
' num_sig ' : 1 ,
' prevout_n ' : output_idx ,
' prevout_hash ' : ctx . txid ( ) ,
' value ' : val ,
' coinbase ' : False ,
' signatures ' : [ None ] ,
} ]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
try :
fee = self . network . config . estimate_fee ( tx_size_bytes )
except NoDynamicFeeEstimates :
fee_per_kb = self . network . config . fee_per_kb ( dyn = False )
fee = self . network . config . estimate_fee_for_feerate ( fee_per_kb , tx_size_bytes )
sweep_outputs = [ ( TYPE_ADDRESS , self . wallet . get_receiving_address ( ) , val - fee ) ]
locktime = self . network . get_local_height ( )
sweep_tx = Transaction . from_io ( sweep_inputs , sweep_outputs , locktime = locktime )
sweep_tx . set_rbf ( True )
sweep_tx . sign ( { our_payment_pubkey : ( our_payment_privkey . get_secret_bytes ( ) , True ) } )
if not sweep_tx . is_complete ( ) :
raise Exception ( ' channel close sweep tx is not complete ' )
return sweep_tx
def find_and_sweep_their_ctx_to_local ( self , ctx , per_commitment_secret : bytes ) :
""" Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point . """
per_commitment_point = ecc . ECPrivkey ( per_commitment_secret ) . get_public_key_bytes ( compressed = True )
revocation_privkey = lnutil . derive_blinded_privkey ( self . chan . local_config . revocation_basepoint . privkey ,
per_commitment_secret )
@ -202,12 +226,25 @@ class LNChanCloseHandler(PrintError):
break
else :
self . print_error ( ' could not find to_local output in their ctx {} ' . format ( ctx . txid ( ) ) )
return
sweep_tx = self . create_sweeptx_ctx_to_local ( ctx , output_idx , witness_script , revocation_privkey , True )
return False
if to_local_address not in self . watched_addresses :
self . watch_address ( to_local_address )
return True
spending_txid = self . wallet . spent_outpoints [ ctx . txid ( ) ] . get ( output_idx )
stx_mined_status = self . get_tx_mined_status ( spending_txid )
if stx_mined_status == TX_MINED_STATUS_DEEP :
return False
elif stx_mined_status in ( TX_MINED_STATUS_SHALLOW , TX_MINED_STATUS_MEMPOOL ) :
return True
sweep_tx = create_sweeptx_ctx_to_local ( self . network , self . wallet , ctx , output_idx ,
witness_script , revocation_privkey , True )
self . network . broadcast_transaction ( sweep_tx ,
lambda res : self . print_tx_broadcast_result ( ' sweep_their_ctx_to_local ' , res ) )
return True
def find_and_sweep_our_ctx_to_local ( self , ctx , our_pcp : bytes ) :
""" Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point . """
delayed_bp_privkey = ecc . ECPrivkey ( self . chan . local_config . delayed_basepoint . privkey )
our_localdelayed_privkey = derive_privkey ( delayed_bp_privkey . secret_scalar , our_pcp )
our_localdelayed_privkey = ecc . ECPrivkey . from_secret_scalar ( our_localdelayed_privkey )
@ -225,49 +262,27 @@ class LNChanCloseHandler(PrintError):
break
else :
self . print_error ( ' could not find to_local output in our ctx {} ' . format ( ctx . txid ( ) ) )
return
# TODO if the CSV lock is still pending, this will fail
sweep_tx = self . create_sweeptx_ctx_to_local ( ctx , output_idx , witness_script ,
our_localdelayed_privkey . get_secret_bytes ( ) ,
False , to_self_delay )
return False
if to_local_address not in self . watched_addresses :
self . watch_address ( to_local_address )
return True
spending_txid = self . wallet . spent_outpoints [ ctx . txid ( ) ] . get ( output_idx )
stx_mined_status = self . get_tx_mined_status ( spending_txid )
if stx_mined_status == TX_MINED_STATUS_DEEP :
return False
elif stx_mined_status in ( TX_MINED_STATUS_SHALLOW , TX_MINED_STATUS_MEMPOOL ) :
return True
# check timelock
ctx_num_conf = self . wallet . get_tx_height ( ctx . txid ( ) ) [ 1 ]
if to_self_delay > ctx_num_conf :
self . print_error ( ' waiting for CSV ( {} < {} ) for ctx {} ' . format ( ctx_num_conf , to_self_delay , ctx . txid ( ) ) )
return True
sweep_tx = create_sweeptx_ctx_to_local ( self . network , self . wallet , ctx , output_idx ,
witness_script , our_localdelayed_privkey . get_secret_bytes ( ) ,
False , to_self_delay )
self . network . broadcast_transaction ( sweep_tx ,
lambda res : self . print_tx_broadcast_result ( ' sweep_our_ctx_to_local ' , res ) )
def create_sweeptx_ctx_to_local ( self , ctx , output_idx : int , witness_script : str ,
privkey : bytes , is_revocation : bool , to_self_delay : int = None ) :
""" Create a txn that sweeps the ' to_local ' output of a commitment
transaction into our wallet .
privkey : either revocation_privkey or localdelayed_privkey
is_revocation : tells us which ^
"""
val = ctx . outputs ( ) [ output_idx ] [ 2 ]
sweep_inputs = [ {
' scriptSig ' : ' ' ,
' type ' : ' p2wsh ' ,
' signatures ' : [ ] ,
' num_sig ' : 0 ,
' prevout_n ' : output_idx ,
' prevout_hash ' : ctx . txid ( ) ,
' value ' : val ,
' coinbase ' : False ,
' preimage_script ' : witness_script ,
} ]
if to_self_delay is not None :
sweep_inputs [ 0 ] [ ' sequence ' ] = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
try :
fee = self . network . config . estimate_fee ( tx_size_bytes )
except NoDynamicFeeEstimates :
fee_per_kb = self . network . config . fee_per_kb ( dyn = False )
fee = self . network . config . estimate_fee_for_feerate ( fee_per_kb , tx_size_bytes )
sweep_outputs = [ ( TYPE_ADDRESS , self . wallet . get_receiving_address ( ) , val - fee ) ]
locktime = self . network . get_local_height ( )
sweep_tx = Transaction . from_io ( sweep_inputs , sweep_outputs , locktime = locktime , version = 2 )
sig = sweep_tx . sign_txin ( 0 , privkey )
witness = transaction . construct_witness ( [ sig , int ( is_revocation ) , witness_script ] )
sweep_tx . inputs ( ) [ 0 ] [ ' witness ' ] = witness
return sweep_tx
return True
def print_tx_broadcast_result ( self , name , res ) :
error = res . get ( ' error ' )
@ -275,3 +290,69 @@ class LNChanCloseHandler(PrintError):
self . print_error ( ' {} broadcast failed: {} ' . format ( name , error ) )
else :
self . print_error ( ' {} broadcast succeeded ' . format ( name ) )
def create_sweeptx_their_ctx_to_remote ( network , wallet , ctx , output_idx : int , our_payment_privkey : ecc . ECPrivkey ) :
our_payment_pubkey = our_payment_privkey . get_public_key_hex ( compressed = True )
val = ctx . outputs ( ) [ output_idx ] [ 2 ]
sweep_inputs = [ {
' type ' : ' p2wpkh ' ,
' x_pubkeys ' : [ our_payment_pubkey ] ,
' num_sig ' : 1 ,
' prevout_n ' : output_idx ,
' prevout_hash ' : ctx . txid ( ) ,
' value ' : val ,
' coinbase ' : False ,
' signatures ' : [ None ] ,
} ]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
try :
fee = network . config . estimate_fee ( tx_size_bytes )
except NoDynamicFeeEstimates :
fee_per_kb = network . config . fee_per_kb ( dyn = False )
fee = network . config . estimate_fee_for_feerate ( fee_per_kb , tx_size_bytes )
sweep_outputs = [ ( TYPE_ADDRESS , wallet . get_receiving_address ( ) , val - fee ) ]
locktime = network . get_local_height ( )
sweep_tx = Transaction . from_io ( sweep_inputs , sweep_outputs , locktime = locktime )
sweep_tx . set_rbf ( True )
sweep_tx . sign ( { our_payment_pubkey : ( our_payment_privkey . get_secret_bytes ( ) , True ) } )
if not sweep_tx . is_complete ( ) :
raise Exception ( ' channel close sweep tx is not complete ' )
return sweep_tx
def create_sweeptx_ctx_to_local ( network , wallet , ctx , output_idx : int , witness_script : str ,
privkey : bytes , is_revocation : bool , to_self_delay : int = None ) :
""" Create a txn that sweeps the ' to_local ' output of a commitment
transaction into our wallet .
privkey : either revocation_privkey or localdelayed_privkey
is_revocation : tells us which ^
"""
val = ctx . outputs ( ) [ output_idx ] [ 2 ]
sweep_inputs = [ {
' scriptSig ' : ' ' ,
' type ' : ' p2wsh ' ,
' signatures ' : [ ] ,
' num_sig ' : 0 ,
' prevout_n ' : output_idx ,
' prevout_hash ' : ctx . txid ( ) ,
' value ' : val ,
' coinbase ' : False ,
' preimage_script ' : witness_script ,
} ]
if to_self_delay is not None :
sweep_inputs [ 0 ] [ ' sequence ' ] = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
try :
fee = network . config . estimate_fee ( tx_size_bytes )
except NoDynamicFeeEstimates :
fee_per_kb = network . config . fee_per_kb ( dyn = False )
fee = network . config . estimate_fee_for_feerate ( fee_per_kb , tx_size_bytes )
sweep_outputs = [ ( TYPE_ADDRESS , wallet . get_receiving_address ( ) , val - fee ) ]
locktime = network . get_local_height ( )
sweep_tx = Transaction . from_io ( sweep_inputs , sweep_outputs , locktime = locktime , version = 2 )
sig = sweep_tx . sign_txin ( 0 , privkey )
witness = transaction . construct_witness ( [ sig , int ( is_revocation ) , witness_script ] )
sweep_tx . inputs ( ) [ 0 ] [ ' witness ' ] = witness
return sweep_tx