Browse Source

move sweeping methods from lnchan.py to new file

also sweep "received" htlcs from "our" ctx
also sweep htlcs from their ctx (non-breach)
extract ctn; included_htlcs_in_their_latest_ctxs
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
SomberNight 6 years ago
committed by ThomasV
parent
commit
595cfcbb65
  1. 2
      electrum/lnbase.py
  2. 312
      electrum/lnchan.py
  3. 493
      electrum/lnsweep.py
  4. 86
      electrum/lnutil.py
  5. 6
      electrum/lnwatcher.py
  6. 2
      electrum/lnworker.py
  7. 21
      electrum/tests/test_lnchan.py
  8. 2
      electrum/tests/test_lnutil.py
  9. 22
      electrum/transaction.py

2
electrum/lnbase.py

@ -500,6 +500,7 @@ class Peer(PrintError):
chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed)
chan.lnwatcher = self.lnwatcher
chan.sweep_address = self.lnworker.sweep_address
chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack.
sig_64, _ = chan.sign_next_commitment()
self.send_message("funding_created",
temporary_channel_id=temp_channel_id,
@ -590,6 +591,7 @@ class Peer(PrintError):
chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed)
chan.lnwatcher = self.lnwatcher
chan.sweep_address = self.lnworker.sweep_address
chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack.
remote_sig = funding_created['signature']
chan.receive_new_commitment(remote_sig, [])
sig_64, _ = chan.sign_next_commitment()

312
electrum/lnchan.py

@ -26,7 +26,7 @@ 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
from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence
from copy import deepcopy
from .util import bfh, PrintError, bh2u
@ -34,17 +34,18 @@ from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from . import ecc
from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction
from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
from .lnutil import get_per_commitment_secret_from_seed
from .lnutil import make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script
from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey
from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_witness
from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey
from .lnutil import sign_and_get_sig_string
from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_commitment_outputs
from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees, RemoteMisbehaving
from .transaction import Transaction, TxOutput, construct_witness
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
from .lnutil import ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script
from .transaction import Transaction
from .lnsweep import (create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx,
create_sweeptxs_for_their_just_revoked_ctx)
class ChannelJsonEncoder(json.JSONEncoder):
def default(self, o):
@ -309,13 +310,16 @@ class Channel(PrintError):
htlcsigs = []
for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]):
for htlc in htlcs:
args = [self.config[REMOTE].next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc]
_script, htlc_tx = make_htlc_tx_with_open_channel(self, *args)
_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])
htlcsigs.append((pending_remote_commitment.htlc_output_indices[htlc.payment_hash], htlc_sig))
self.process_new_offchain_ctx(pending_remote_commitment, ours=False)
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]
@ -383,11 +387,14 @@ class Channel(PrintError):
if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]:
self.pending_fee[FUNDER_SIGNED] = True
self.process_new_offchain_ctx(pending_local_commitment, ours=True)
def verify_htlc(self, htlc, htlc_sigs, we_receive):
_, this_point, _ = self.points()
_script, htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, self.pending_commitment(LOCAL), htlc)
def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool) -> int:
_, this_point, _ = self.points
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
pcp=this_point,
for_us=True,
we_receive=we_receive,
commit=self.pending_commitment(LOCAL),
htlc=htlc)
pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point)
for idx, sig in enumerate(htlc_sigs):
@ -396,6 +403,13 @@ class Channel(PrintError):
else:
raise Exception(f'failed verifying HTLC signatures: {htlc}')
def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool) -> 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)
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")
@ -435,36 +449,18 @@ class Channel(PrintError):
next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big'))
return last_secret, this_point, next_point
# TODO batch sweeps
# TODO sweep HTLC outputs
def process_new_offchain_ctx(self, ctx, ours: bool):
def process_new_revocation_secret(self, per_commitment_secret: bytes):
if not self.lnwatcher:
return
outpoint = self.funding_outpoint.to_str()
if ours:
ctn = self.config[LOCAL].ctn + 1
our_per_commitment_secret = get_per_commitment_secret_from_seed(
self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_cur_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
encumbered_sweeptxs = create_sweeptxs_for_our_ctx(self, ctx, our_cur_pcp, self.sweep_address)
else:
their_cur_pcp = self.config[REMOTE].next_per_commitment_point
encumbered_sweeptxs = [(None, maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address))]
ctx = self.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it?
encumbered_sweeptxs = create_sweeptxs_for_their_just_revoked_ctx(self, ctx, per_commitment_secret, self.sweep_address)
for prev_txid, encumbered_tx in encumbered_sweeptxs:
if prev_txid is None:
prev_txid = ctx.txid()
if encumbered_tx is not None:
self.lnwatcher.add_sweep_tx(outpoint, prev_txid, encumbered_tx.to_json())
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
encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(self, ctx, per_commitment_secret, self.sweep_address)
if encumbered_sweeptx:
self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx.to_json())
def receive_revocation(self, revocation) -> Tuple[int, int]:
self.print_error("receive_revocation")
@ -476,12 +472,6 @@ class Channel(PrintError):
self.log = old_logs
raise Exception('revoked secret not for current point')
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
# 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
@ -490,6 +480,14 @@ class Channel(PrintError):
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
def mark_settled(subject):
"""
find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs
@ -768,21 +766,19 @@ class Channel(PrintError):
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 = []
htlcs = [] # type: List[ScriptHtlc]
def append_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool):
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))
for htlc in self.included_htlcs(subject, -subject):
htlcs.append( ScriptHtlc( make_received_htlc(
other_revocation_pubkey,
other_htlc_pubkey,
this_htlc_pubkey,
htlc.payment_hash,
htlc.cltv_expiry), htlc))
append_htlc(htlc, is_received_htlc=True)
for htlc in self.included_htlcs(subject, subject):
htlcs.append(
ScriptHtlc( make_offered_htlc(
other_revocation_pubkey,
other_htlc_pubkey,
this_htlc_pubkey,
htlc.payment_hash), htlc))
append_htlc(htlc, is_received_htlc=False)
if subject != LOCAL:
remote_msat, local_msat = local_msat, remote_msat
payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
@ -851,209 +847,15 @@ class Channel(PrintError):
assert tx.is_complete()
return tx
def included_htlcs_in_latest_ctxs(self):
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. """
old_htlcs = list(self.included_htlcs(REMOTE, REMOTE, only_pending=False)) \
+ list(self.included_htlcs(REMOTE, LOCAL, only_pending=False))
old_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator, only_pending=False))
old_logs = dict(self.lock_in_htlc_changes(LOCAL))
new_htlcs = list(self.included_htlcs(REMOTE, REMOTE)) \
+ list(self.included_htlcs(REMOTE, LOCAL))
new_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator))
self.log = old_logs
return {self.config[REMOTE].ctn: old_htlcs,
self.config[REMOTE].ctn+1: new_htlcs, }
def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(their_pcp, bytes)
payment_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].payment_basepoint.privkey)
our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
if type_ == TYPE_ADDRESS and addr == to_remote_address:
break
else:
return None
sweep_tx = create_sweeptx_their_ctx_to_remote(address=sweep_address,
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey)
return EncumberedTransaction('their_ctx_to_remote', sweep_tx, csv_delay=0, cltv_expiry=0)
def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret: bytes,
sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(per_commitment_secret, bytes)
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
revocation_privkey = derive_blinded_privkey(chan.config[LOCAL].revocation_basepoint.privkey,
per_commitment_secret)
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
to_self_delay = chan.config[LOCAL].to_self_delay
delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey,
per_commitment_point)
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
for output_idx, o in enumerate(ctx.outputs()):
if o.type == TYPE_ADDRESS and o.address == to_local_address:
break
else:
return None
sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=revocation_privkey,
is_revocation=True)
return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0)
def create_sweeptxs_for_our_ctx(chan, ctx, our_pcp: bytes, sweep_address) \
-> List[Tuple[Optional[str],EncumberedTransaction]]:
assert isinstance(our_pcp, bytes)
delayed_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].delayed_basepoint.privkey)
our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey,
our_pcp)
to_self_delay = chan.config[REMOTE].to_self_delay
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
txs = []
for output_idx, o in enumerate(ctx.outputs()):
if o.type == TYPE_ADDRESS and o.address == to_local_address:
sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address,
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
to_self_delay=to_self_delay)
txs.append((None, EncumberedTransaction('our_ctx_to_local', sweep_tx, csv_delay=to_self_delay, cltv_expiry=0)))
break
# TODO htlc successes
htlcs = list(chan.included_htlcs(LOCAL, LOCAL)) # timeouts
for htlc in htlcs:
witness_script, htlc_tx = make_htlc_tx_with_open_channel(
chan,
our_pcp,
True, # for_us
False, # we_receive
ctx, htlc)
data = chan.config[LOCAL].current_htlc_signatures
htlc_sigs = [data[i:i+64] for i in range(0, len(data), 64)]
idx = chan.verify_htlc(htlc, htlc_sigs, False)
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01'
remote_revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey, our_pcp)
remote_htlc_pubkey = derive_pubkey(chan.config[REMOTE].htlc_basepoint.pubkey, our_pcp)
local_htlc_key = derive_privkey(
int.from_bytes(chan.config[LOCAL].htlc_basepoint.privkey, 'big'),
our_pcp).to_bytes(32, 'big')
program = make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, privkey_to_pubkey(local_htlc_key), htlc.payment_hash)
local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_key))
htlc_tx.inputs()[0]['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, b'', program))
tx_size_bytes = 999 # TODO
fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
second_stage_outputs = [TxOutput(TYPE_ADDRESS, chan.sweep_address, htlc.amount_msat // 1000 - fee)]
assert to_self_delay is not None
second_stage_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
'signatures': [],
'num_sig': 0,
'prevout_n': 0,
'prevout_hash': htlc_tx.txid(),
'value': htlc_tx.outputs()[0].value,
'coinbase': False,
'preimage_script': bh2u(witness_script),
'sequence': to_self_delay,
}]
tx = Transaction.from_io(second_stage_inputs, second_stage_outputs, version=2)
local_delaykey = derive_privkey(
int.from_bytes(chan.config[LOCAL].delayed_basepoint.privkey, 'big'),
our_pcp).to_bytes(32, 'big')
assert local_delaykey == our_localdelayed_privkey.get_secret_bytes()
witness = construct_witness([bfh(tx.sign_txin(0, local_delaykey)), 0, witness_script])
tx.inputs()[0]['witness'] = witness
assert tx.is_complete()
txs.append((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', tx, csv_delay=to_self_delay, cltv_expiry=0)))
txs.append((ctx.txid(), EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry)))
return txs
def create_sweeptx_their_ctx_to_remote(address, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey,
fee_per_kb: int=None) -> Transaction:
our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
val = ctx.outputs()[output_idx].value
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
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val-fee)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
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(address, ctx, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool,
to_self_delay: int=None,
fee_per_kb: int=None) -> Transaction:
"""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].value
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
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val - fee)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = sweep_tx.sign_txin(0, privkey)
witness = construct_witness([sig, int(is_revocation), witness_script])
sweep_tx.inputs()[0]['witness'] = witness
return sweep_tx

493
electrum/lnsweep.py

@ -0,0 +1,493 @@
# Copyright (C) 2018 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING
from .util import bfh, bh2u, print_error
from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
from . import ecc
from .lnutil import (EncumberedTransaction,
make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
make_htlc_tx_witness, make_htlc_tx_with_open_channel,
LOCAL, REMOTE, make_htlc_output_witness_script, UnknownPaymentHash,
get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret)
from .transaction import Transaction, TxOutput, construct_witness
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
if TYPE_CHECKING:
from .lnchan import Channel, UpdateAddHtlc
def maybe_create_sweeptx_for_their_ctx_to_remote(ctx: Transaction, sweep_address: str,
our_payment_privkey: ecc.ECPrivkey) -> Optional[Transaction]:
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
output_idx = ctx.get_output_idx_from_address(to_remote_address)
if output_idx is None: return None
sweep_tx = create_sweeptx_their_ctx_to_remote(sweep_address=sweep_address,
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey)
return sweep_tx
def maybe_create_sweeptx_for_their_ctx_to_local(ctx: Transaction, revocation_privkey: bytes,
to_self_delay: int, delayed_pubkey: bytes,
sweep_address: str) -> Optional[EncumberedTransaction]:
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
output_idx = ctx.get_output_idx_from_address(to_local_address)
if output_idx is None: return None
sweep_tx = create_sweeptx_ctx_to_local(sweep_address=sweep_address,
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=revocation_privkey,
is_revocation=True)
if sweep_tx is None: return None
return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0)
def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]:
"""Presign sweeping transactions using the just received revoked pcs.
These will only be utilised if the remote breaches.
Sweep 'lo_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).
"""
# prep
pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)
other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey,
per_commitment_secret)
to_self_delay = other_conf.to_self_delay
this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)
txs = []
# to_local
sweep_tx = maybe_create_sweeptx_for_their_ctx_to_local(ctx=ctx,
revocation_privkey=other_revocation_privkey,
to_self_delay=to_self_delay,
delayed_pubkey=this_delayed_pubkey,
sweep_address=sweep_address)
if sweep_tx:
txs.append((None, EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0)))
# HTLCs
def create_sweeptx_for_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool) -> Tuple[Optional[Transaction],
Optional[Transaction],
Transaction]:
htlc_tx_witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan,
pcp=pcp,
for_us=False,
we_receive=not is_received_htlc,
commit=ctx,
htlc=htlc)
htlc_tx_txin = htlc_tx.inputs()[0]
htlc_output_witness_script = bfh(Transaction.get_preimage_script(htlc_tx_txin))
# sweep directly from ctx
direct_sweep_tx = maybe_create_sweeptx_for_their_ctx_htlc(
ctx=ctx,
sweep_address=sweep_address,
htlc_output_witness_script=htlc_output_witness_script,
privkey=other_revocation_privkey,
preimage=None,
is_revocation=True)
# sweep from htlc tx
secondstage_sweep_tx = create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
htlc_tx=htlc_tx,
htlctx_witness_script=htlc_tx_witness_script,
sweep_address=sweep_address,
privkey=other_revocation_privkey,
is_revocation=True)
return direct_sweep_tx, secondstage_sweep_tx, htlc_tx
# received HTLCs, in their ctx
# TODO consider carefully if "included_htlcs" is what we need here
received_htlcs = list(chan.included_htlcs(REMOTE, LOCAL)) # type: List[UpdateAddHtlc]
for htlc in received_htlcs:
direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True)
if direct_sweep_tx:
txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', direct_sweep_tx, csv_delay=0, cltv_expiry=0)))
if secondstage_sweep_tx:
txs.append((htlc_tx.txid(), EncumberedTransaction(f'their_htlctx_{bh2u(htlc.payment_hash)}', secondstage_sweep_tx, csv_delay=0, cltv_expiry=0)))
# offered HTLCs, in their ctx
# TODO consider carefully if "included_htlcs" is what we need here
offered_htlcs = list(chan.included_htlcs(REMOTE, REMOTE)) # type: List[UpdateAddHtlc]
for htlc in offered_htlcs:
direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False)
if direct_sweep_tx:
txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', direct_sweep_tx, csv_delay=0, cltv_expiry=0)))
if secondstage_sweep_tx:
txs.append((htlc_tx.txid(), EncumberedTransaction(f'their_htlctx_{bh2u(htlc.payment_hash)}', secondstage_sweep_tx, csv_delay=0, cltv_expiry=0)))
return txs
def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction,
sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]:
"""Handle the case where we force close unilaterally with our latest ctx.
Construct sweep txns for 'to_local', and for all HTLCs (2 txns each).
'to_local' can be swept even if this is a breach (by us),
but HTLCs cannot (old HTLCs are no longer stored).
"""
this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
our_per_commitment_secret = get_per_commitment_secret_from_seed(
this_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
# prep
this_delayed_bp_privkey = ecc.ECPrivkey(this_conf.delayed_basepoint.privkey)
this_localdelayed_privkey = derive_privkey(this_delayed_bp_privkey.secret_scalar, our_pcp)
this_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(this_localdelayed_privkey)
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, our_pcp)
to_self_delay = chan.config[REMOTE].to_self_delay
this_htlc_privkey = derive_privkey(secret=int.from_bytes(this_conf.htlc_basepoint.privkey, 'big'),
per_commitment_point=our_pcp).to_bytes(32, 'big')
txs = []
# to_local
sweep_tx = maybe_create_sweeptx_that_spends_to_local_in_our_ctx(ctx=ctx,
sweep_address=sweep_address,
our_localdelayed_privkey=this_localdelayed_privkey,
remote_revocation_pubkey=other_revocation_pubkey,
to_self_delay=to_self_delay)
if sweep_tx:
txs.append((None, EncumberedTransaction('our_ctx_to_local', sweep_tx, csv_delay=to_self_delay, cltv_expiry=0)))
# HTLCs
def create_txns_for_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool) -> Tuple[Optional[Transaction], Optional[Transaction]]:
if is_received_htlc:
try:
preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash)
except UnknownPaymentHash as e:
print_error(f'trying to sweep htlc from our latest ctx but getting {repr(e)}')
return None, None
else:
preimage = None
htlctx_witness_script, htlc_tx = create_htlctx_that_spends_from_our_ctx(
chan=chan,
our_pcp=our_pcp,
ctx=ctx,
htlc=htlc,
local_htlc_privkey=this_htlc_privkey,
preimage=preimage,
is_received_htlc=is_received_htlc)
to_wallet_tx = create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
to_self_delay=to_self_delay,
htlc_tx=htlc_tx,
htlctx_witness_script=htlctx_witness_script,
sweep_address=sweep_address,
privkey=this_localdelayed_privkey.get_secret_bytes(),
is_revocation=False)
return htlc_tx, to_wallet_tx
# offered HTLCs, in our ctx --> "timeout"
# TODO consider carefully if "included_htlcs" is what we need here
offered_htlcs = list(chan.included_htlcs(LOCAL, LOCAL)) # type: List[UpdateAddHtlc]
for htlc in offered_htlcs:
htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=False)
if htlc_tx and to_wallet_tx:
txs.append((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0)))
txs.append((ctx.txid(), EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry)))
# received HTLCs, in our ctx --> "success"
# TODO consider carefully if "included_htlcs" is what we need here
received_htlcs = list(chan.included_htlcs(LOCAL, REMOTE)) # type: List[UpdateAddHtlc]
for htlc in received_htlcs:
htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=True)
if htlc_tx and to_wallet_tx:
txs.append((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0)))
txs.append((ctx.txid(), EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=0)))
return txs
def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction,
sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]:
"""Handle the case when the remote force-closes with their ctx.
Regardless of it is a breach or not, construct sweep tx for 'to_remote'.
If it is a breach, also construct sweep tx for 'to_local'.
Sweep txns for HTLCs are only constructed if it is NOT a breach, as
lnchan does not store old HTLCs.
"""
this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
# note: the remote sometimes has two valid non-revoked commitment transactions,
# either of which could be broadcast (this_conf.ctn, this_conf.ctn+1)
per_commitment_secret = None
if ctn == this_conf.ctn:
their_pcp = this_conf.current_per_commitment_point
elif ctn == this_conf.ctn + 1:
their_pcp = this_conf.next_per_commitment_point
elif ctn < this_conf.ctn: # breach
try:
per_commitment_secret = this_conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
except UnableToDeriveSecret:
return []
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
else:
return []
# prep
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, their_pcp)
other_htlc_privkey = derive_privkey(secret=int.from_bytes(other_conf.htlc_basepoint.privkey, 'big'),
per_commitment_point=their_pcp)
other_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(other_htlc_privkey)
this_htlc_pubkey = derive_pubkey(this_conf.htlc_basepoint.pubkey, their_pcp)
other_payment_bp_privkey = ecc.ECPrivkey(other_conf.payment_basepoint.privkey)
other_payment_privkey = derive_privkey(other_payment_bp_privkey.secret_scalar, their_pcp)
other_payment_privkey = ecc.ECPrivkey.from_secret_scalar(other_payment_privkey)
txs = []
if per_commitment_secret: # breach
# to_local
other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey,
per_commitment_secret)
this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, their_pcp)
sweep_tx = maybe_create_sweeptx_for_their_ctx_to_local(ctx=ctx,
revocation_privkey=other_revocation_privkey,
to_self_delay=other_conf.to_self_delay,
delayed_pubkey=this_delayed_pubkey,
sweep_address=sweep_address)
if sweep_tx:
txs.append((None, EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0)))
# to_remote
sweep_tx = maybe_create_sweeptx_for_their_ctx_to_remote(ctx=ctx,
sweep_address=sweep_address,
our_payment_privkey=other_payment_privkey)
if sweep_tx:
txs.append((None, EncumberedTransaction('their_ctx_to_remote', sweep_tx, csv_delay=0, cltv_expiry=0)))
# HTLCs
# from their ctx, we can only redeem HTLCs if the ctx was not revoked,
# as old HTLCs are not stored. (if it was revoked, then we should have presigned txns
# to handle the breach already; out of scope here)
if ctn not in (this_conf.ctn, this_conf.ctn + 1):
return txs
def create_sweeptx_for_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool) -> Optional[Transaction]:
if not is_received_htlc:
try:
preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash)
except UnknownPaymentHash as e:
print_error(f'trying to sweep htlc from their latest ctx but getting {repr(e)}')
return None
else:
preimage = None
htlc_output_witness_script = make_htlc_output_witness_script(
is_received_htlc=is_received_htlc,
remote_revocation_pubkey=other_revocation_pubkey,
remote_htlc_pubkey=other_htlc_privkey.get_public_key_bytes(compressed=True),
local_htlc_pubkey=this_htlc_pubkey,
payment_hash=htlc.payment_hash,
cltv_expiry=htlc.cltv_expiry)
sweep_tx = maybe_create_sweeptx_for_their_ctx_htlc(
ctx=ctx,
sweep_address=sweep_address,
htlc_output_witness_script=htlc_output_witness_script,
privkey=other_htlc_privkey.get_secret_bytes(),
preimage=preimage,
is_revocation=False)
return sweep_tx
# received HTLCs, in their ctx --> "timeout"
received_htlcs = chan.included_htlcs_in_their_latest_ctxs(LOCAL)[ctn] # type: List[UpdateAddHtlc]
for htlc in received_htlcs:
sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True)
if sweep_tx:
txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry)))
# offered HTLCs, in their ctx --> "success"
offered_htlcs = chan.included_htlcs_in_their_latest_ctxs(REMOTE)[ctn] # type: List[UpdateAddHtlc]
for htlc in offered_htlcs:
sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False)
if sweep_tx:
txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=0)))
return txs
def maybe_create_sweeptx_that_spends_to_local_in_our_ctx(
ctx: Transaction, sweep_address: str, our_localdelayed_privkey: ecc.ECPrivkey,
remote_revocation_pubkey: bytes, to_self_delay: int) -> Optional[Transaction]:
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
to_local_witness_script = bh2u(make_commitment_output_to_local_witness_script(
remote_revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script)
output_idx = ctx.get_output_idx_from_address(to_local_address)
if output_idx is None: return None
sweep_tx = create_sweeptx_ctx_to_local(sweep_address=sweep_address,
ctx=ctx,
output_idx=output_idx,
witness_script=to_local_witness_script,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
to_self_delay=to_self_delay)
if sweep_tx is None: return None
return sweep_tx
def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes,
ctx: Transaction, htlc: 'UpdateAddHtlc',
local_htlc_privkey: bytes, preimage: Optional[bytes],
is_received_htlc: bool) -> Tuple[bytes, Transaction]:
assert is_received_htlc == bool(preimage), 'preimage is required iff htlc is received'
preimage = preimage or b''
witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan,
pcp=our_pcp,
for_us=True,
we_receive=is_received_htlc,
commit=ctx,
htlc=htlc)
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc, we_receive=is_received_htlc)
local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
txin = htlc_tx.inputs()[0]
witness_program = bfh(Transaction.get_preimage_script(txin))
txin['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program))
return witness_script, htlc_tx
def maybe_create_sweeptx_for_their_ctx_htlc(ctx: Transaction, sweep_address: str,
htlc_output_witness_script: bytes,
privkey: bytes, is_revocation: bool,
preimage: Optional[bytes]) -> Optional[Transaction]:
htlc_address = redeem_script_to_address('p2wsh', bh2u(htlc_output_witness_script))
# FIXME handle htlc_address collision
# also: https://github.com/lightningnetwork/lightning-rfc/issues/448
output_idx = ctx.get_output_idx_from_address(htlc_address)
if output_idx is None: return None
sweep_tx = create_sweeptx_their_ctx_htlc(ctx=ctx,
witness_script=htlc_output_witness_script,
sweep_address=sweep_address,
preimage=preimage,
output_idx=output_idx,
privkey=privkey,
is_revocation=is_revocation)
return sweep_tx
def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str,
preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool,
fee_per_kb: int=None) -> Optional[Transaction]:
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
sweep_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
'signatures': [],
'num_sig': 0,
'prevout_n': output_idx,
'prevout_hash': ctx.txid(),
'value': val,
'coinbase': False,
'preimage_script': bh2u(witness_script),
}]
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation)
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = bfh(tx.sign_txin(0, privkey))
if not is_revocation:
witness = construct_witness([sig, preimage, witness_script])
else:
revocation_pubkey = privkey_to_pubkey(privkey)
witness = construct_witness([sig, revocation_pubkey, witness_script])
tx.inputs()[0]['witness'] = witness
assert tx.is_complete()
return tx
def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int,
our_payment_privkey: ecc.ECPrivkey,
fee_per_kb: int=None) -> Optional[Transaction]:
our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
val = ctx.outputs()[output_idx].value
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
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
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(sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool,
to_self_delay: int=None,
fee_per_kb: int=None) -> Optional[Transaction]:
"""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].value
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 not is_revocation:
assert isinstance(to_self_delay, int)
sweep_inputs[0]['sequence'] = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = sweep_tx.sign_txin(0, privkey)
witness = construct_witness([sig, int(is_revocation), witness_script])
sweep_tx.inputs()[0]['witness'] = witness
return sweep_tx
def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str,
privkey: bytes, is_revocation: bool, to_self_delay: int=None,
fee_per_kb: int=None) -> Optional[Transaction]:
val = htlc_tx.outputs()[0].value
sweep_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
'signatures': [],
'num_sig': 0,
'prevout_n': 0,
'prevout_hash': htlc_tx.txid(),
'value': val,
'coinbase': False,
'preimage_script': bh2u(htlctx_witness_script),
}]
if not is_revocation:
assert isinstance(to_self_delay, int)
sweep_inputs[0]['sequence'] = to_self_delay
tx_size_bytes = 200 # TODO
if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = bfh(tx.sign_txin(0, privkey))
witness = construct_witness([sig, int(is_revocation), htlctx_witness_script])
tx.inputs()[0]['witness'] = witness
assert tx.is_complete()
return tx

86
electrum/lnutil.py

@ -5,7 +5,7 @@
from enum import IntFlag, IntEnum
import json
from collections import namedtuple
from typing import NamedTuple, List, Tuple, Mapping, Optional
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union
import re
from .util import bfh, bh2u, inv_dict
@ -13,13 +13,16 @@ from .crypto import sha256
from .transaction import Transaction
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction
from .transaction import opcodes, TxOutput
from .bitcoin import push_script
from .transaction import opcodes, TxOutput, Transaction
from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS
from . import segwit_addr
from .i18n import _
from .lnaddr import lndecode
from .keystore import BIP32_KeyStore
if TYPE_CHECKING:
from .lnchan import Channel, UpdateAddHtlc
HTLC_TIMEOUT_WEIGHT = 663
HTLC_SUCCESS_WEIGHT = 703
@ -238,18 +241,18 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela
output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat)
return script, output
def make_htlc_tx_witness(remotehtlcsig, localhtlcsig, payment_preimage, witness_script):
def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
payment_preimage: bytes, witness_script: bytes) -> bytes:
assert type(remotehtlcsig) is bytes
assert type(localhtlcsig) is bytes
assert type(payment_preimage) is bytes
assert type(witness_script) is bytes
return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script]))
def make_htlc_tx_inputs(htlc_output_txid, htlc_output_index, revocationpubkey, local_delayedpubkey, amount_msat, witness_script):
def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
amount_msat: int, witness_script: str) -> List[dict]:
assert type(htlc_output_txid) is str
assert type(htlc_output_index) is int
assert type(revocationpubkey) is bytes
assert type(local_delayedpubkey) is bytes
assert type(amount_msat) is int
assert type(witness_script) is str
c_inputs = [{
@ -272,7 +275,8 @@ def make_htlc_tx(cltv_timeout, inputs, output):
tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_timeout, version=2)
return tx
def make_offered_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash):
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
local_htlcpubkey: bytes, payment_hash: bytes) -> bytes:
assert type(revocation_pubkey) is bytes
assert type(remote_htlcpubkey) is bytes
assert type(local_htlcpubkey) is bytes
@ -285,7 +289,8 @@ def make_offered_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, pa
+ bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_HASH160])\
+ bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) + bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF])
def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash, cltv_expiry):
def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes:
for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]:
assert type(i) is bytes
assert type(cltv_expiry) is int
@ -307,12 +312,34 @@ def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, p
+ bitcoin.add_number_to_script(cltv_expiry) \
+ bytes([opcodes.OP_CLTV, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF])
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
def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes,
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes:
if is_received_htlc:
return make_received_htlc(revocation_pubkey=remote_revocation_pubkey,
remote_htlcpubkey=remote_htlc_pubkey,
local_htlcpubkey=local_htlc_pubkey,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry)
else:
return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey,
remote_htlcpubkey=remote_htlc_pubkey,
local_htlcpubkey=local_htlc_pubkey,
payment_hash=payment_hash)
def get_ordered_channel_configs(chan: 'Channel', for_us: bool) -> Tuple[Union[LocalConfig, RemoteConfig],
Union[LocalConfig, RemoteConfig]]:
conf = chan.config[LOCAL] if for_us else chan.config[REMOTE]
other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE]
return conf, other_conf
def make_htlc_tx_with_open_channel(chan: 'Channel', pcp: bytes, for_us: bool,
we_receive: bool, commit: Transaction,
htlc: 'UpdateAddHtlc') -> Tuple[bytes, Transaction]:
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp)
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)
@ -323,19 +350,23 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc):
script, htlc_tx_output = make_htlc_tx_output(
amount_msat = amount_msat,
local_feerate = chan.pending_feerate(LOCAL if for_us else REMOTE),
revocationpubkey=revocation_pubkey,
revocationpubkey=other_revocation_pubkey,
local_delayedpubkey=delayedpubkey,
success = is_htlc_success,
to_self_delay = other_conf.to_self_delay)
if is_htlc_success:
preimage_script = make_received_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash, cltv_expiry)
else:
preimage_script = make_offered_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash)
output_idx = commit.htlc_output_indices[htlc.payment_hash]
preimage_script = make_htlc_output_witness_script(is_received_htlc=is_htlc_success,
remote_revocation_pubkey=other_revocation_pubkey,
remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=htlc_pubkey,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry)
htlc_address = redeem_script_to_address('p2wsh', bh2u(preimage_script))
# FIXME handle htlc_address collision
# also: https://github.com/lightningnetwork/lightning-rfc/issues/448
prevout_idx = commit.get_output_idx_from_address(htlc_address)
assert prevout_idx is not None
htlc_tx_inputs = make_htlc_tx_inputs(
commit.txid(), output_idx,
revocationpubkey=revocation_pubkey,
local_delayedpubkey=delayedpubkey,
commit.txid(), prevout_idx,
amount_msat=amount_msat,
witness_script=bh2u(preimage_script))
if is_htlc_success:
@ -401,7 +432,7 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, fees_per_participant,
htlcs):
htlcs: List[ScriptHtlc]) -> Transaction:
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat)
obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)
@ -423,15 +454,6 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
# create commitment tx
tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
tx.htlc_output_indices = {}
assert len(htlcs) == len(htlc_outputs)
for script_htlc, output in zip(htlcs, htlc_outputs):
if output in tx.outputs():
# minus the first two outputs (to_local, to_remote)
assert script_htlc.htlc.payment_hash not in tx.htlc_output_indices
tx.htlc_output_indices[script_htlc.htlc.payment_hash] = tx.outputs().index(output)
return tx
def make_commitment_output_to_local_witness_script(
@ -487,7 +509,7 @@ def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes,
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
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: Transaction, chan: 'Channel') -> int:
funder_conf = chan.config[LOCAL] if chan.constraints.is_initiator else chan.config[REMOTE]
fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE]
return extract_ctn_from_tx(tx, txin_index=0,

6
electrum/lnwatcher.py

@ -36,9 +36,6 @@ class TxMinedDepth(IntEnum):
class LNWatcher(AddressSynchronizer):
# 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
verbosity_filter = 'W'
def __init__(self, network: 'Network'):
@ -181,6 +178,7 @@ class LNWatcher(AddressSynchronizer):
if self.get_tx_mined_depth(prev_txid) == TxMinedDepth.DEEP:
self.print_error('have no follow-up transactions and prevtx', prev_txid, 'mined deep, returning')
return False
return True
# check if any response applies
keep_watching_this = False
local_height = self.network.get_local_height()
@ -241,7 +239,7 @@ class LNWatcher(AddressSynchronizer):
def get_tx_mined_depth(self, txid: str):
if not txid:
return TxMinedStatus.FREE
return TxMinedDepth.FREE
tx_mined_depth = self.get_tx_height(txid)
height, conf = tx_mined_depth.height, tx_mined_depth.conf
if conf > 100:

2
electrum/lnworker.py

@ -531,6 +531,8 @@ class LNWorker(PrintError):
return routing_hints
def delete_invoice(self, payment_hash_hex: str):
# FIXME we will now LOSE the preimage!! is this feature a good idea?
# maybe instead of deleting, we could have a feature to "hide" invoices (e.g. for GUI)
try:
del self.invoices[payment_hash_hex]
except KeyError:

21
electrum/tests/test_lnchan.py

@ -199,10 +199,10 @@ class TestChannel(unittest.TestCase):
alice_channel, bob_channel = self.alice_channel, self.bob_channel
htlc = self.htlc
ctn_to_htlcs = alice_channel.included_htlcs_in_latest_ctxs()
self.assertEqual(list(ctn_to_htlcs.keys()), [0,1])
self.assertEqual(ctn_to_htlcs[0], [])
self.assertEqual(ctn_to_htlcs[1], [htlc])
self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
# Next alice commits this change by sending a signature message. Since
# we expect the messages to be ordered, Bob will receive the HTLC we
@ -217,6 +217,11 @@ class TestChannel(unittest.TestCase):
# from Alice.
bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)
self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
# Bob revokes his prior commitment given to him by Alice, since he now
# has a valid signature for a newer commitment.
bobRevocation, _ = bob_channel.revoke_current_commitment()
@ -279,10 +284,10 @@ class TestChannel(unittest.TestCase):
bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()
ctn_to_htlcs = bob_channel.included_htlcs_in_latest_ctxs()
self.assertEqual(list(ctn_to_htlcs.keys()), [1,2])
self.assertEqual(len(ctn_to_htlcs[1]), 1)
self.assertEqual(len(ctn_to_htlcs[2]), 0)
self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
self.assertEqual({1: [htlc], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({1: [], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE))
self.assertEqual({1: [], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL))
alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)

2
electrum/tests/test_lnutil.py

@ -552,8 +552,6 @@ class TestLNUtil(unittest.TestCase):
our_htlc_tx_inputs = make_htlc_tx_inputs(
htlc_output_txid=our_commit_tx.txid(),
htlc_output_index=htlc_output_index,
revocationpubkey=local_revocation_pubkey,
local_delayedpubkey=local_delayedpubkey,
amount_msat=amount_msat,
witness_script=bh2u(htlc))
our_htlc_tx = make_htlc_tx(cltv_timeout,

22
electrum/transaction.py

@ -1190,6 +1190,28 @@ class Transaction:
return (addr in (o.address for o in self.outputs())) \
or (addr in (txin.get("address") for txin in self.inputs()))
def get_output_idx_from_scriptpubkey(self, script: str) -> Optional[int]:
"""Returns the index of an output with given script.
If there are no such outputs, returns None;
if there are multiple, returns one of them.
"""
assert isinstance(script, str) # hex
# build cache if there isn't one yet
# note: can become stale and return incorrect data
# if the tx is modified later; that's out of scope.
if not hasattr(self, '_script_to_output_idx'):
d = {}
for output_idx, o in enumerate(self.outputs()):
o_script = self.pay_script(o.type, o.address)
assert isinstance(o_script, str)
d[o_script] = output_idx
self._script_to_output_idx = d
return self._script_to_output_idx.get(script)
def get_output_idx_from_address(self, addr: str) -> Optional:
script = bitcoin.address_to_script(addr)
return self.get_output_idx_from_scriptpubkey(script)
def as_dict(self):
if self.raw is None:
self.raw = self.serialize()

Loading…
Cancel
Save