Browse Source

redeem htlc outputs of our local commitment transaction back to wallet

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
1f97a9753e
  1. 6
      electrum/lnbase.py
  2. 134
      electrum/lnchan.py
  3. 18
      electrum/lnutil.py
  4. 78
      electrum/lnwatcher.py

6
electrum/lnbase.py

@ -939,6 +939,7 @@ class Peer(PrintError):
# create onion packet
final_cltv = self.network.get_local_height() + min_final_cltv_expiry
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv)
assert final_cltv <= cltv, (final_cltv, cltv)
secret_key = os.urandom(32)
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
chan.check_can_pay(amount_msat)
@ -973,11 +974,6 @@ class Peer(PrintError):
def on_commitment_signed(self, payload):
self.print_error("commitment_signed", payload)
channel_id = payload['channel_id']
chan = self.channels[channel_id]
chan.config[LOCAL]=chan.config[LOCAL]._replace(
current_commitment_signature=payload['signature'],
current_htlc_signatures=payload['htlc_signature'])
self.lnworker.save_channel(chan)
self.commitment_signed[channel_id].put_nowait(payload)
@log_exceptions

134
electrum/lnchan.py

@ -3,7 +3,7 @@ from collections import namedtuple, defaultdict
import binascii
import json
from enum import Enum, auto
from typing import Optional, Mapping, List
from typing import Optional, Dict, List, Tuple
from .util import bfh, PrintError, bh2u
from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS
@ -14,7 +14,7 @@ from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeyp
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
from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_witness
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_outputs
@ -129,8 +129,8 @@ class Channel(PrintError):
self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"])
template = lambda: {
'adds': {}, # type: Mapping[HTLC_ID, UpdateAddHtlc]
'settles': [], # type: List[HTLC_ID]
'adds': {}, # Dict[HTLC_ID, UpdateAddHtlc]
'settles': [], # List[HTLC_ID]
}
self.log = {LOCAL: template(), REMOTE: template()}
for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
@ -244,7 +244,7 @@ class Channel(PrintError):
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]
htlc_tx = make_htlc_tx_with_open_channel(self, *args)
_script, htlc_tx = make_htlc_tx_with_open_channel(self, *args)
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
htlcsigs.append((pending_remote_commitment.htlc_output_indices[htlc.payment_hash], htlc_sig))
@ -286,22 +286,20 @@ class Channel(PrintError):
if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash):
raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex)
_, this_point, _ = self.points
htlc_sigs_string = b''.join(htlc_sigs)
htlc_sigs = htlc_sigs[:] # copy cause we will delete now
for htlcs, we_receive in [(self.included_htlcs(LOCAL, REMOTE), True), (self.included_htlcs(LOCAL, LOCAL), False)]:
for htlc in htlcs:
htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc)
pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point)
for idx, sig in enumerate(htlc_sigs):
if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
del htlc_sigs[idx]
break
else:
raise Exception(f'failed verifying HTLC signatures: {htlc}')
idx = self.verify_htlc(htlc, htlc_sigs, we_receive)
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)
for pending_fee in self.fee_mgr:
if not self.constraints.is_initiator:
pending_fee[FUNDEE_SIGNED] = True
@ -310,6 +308,16 @@ class Channel(PrintError):
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_local_commitment, htlc)
pre_hash = Hash(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):
if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash):
return idx
else:
raise Exception(f'failed verifying HTLC signatures: {htlc}')
def revoke_current_commitment(self):
"""
@ -372,12 +380,14 @@ class Channel(PrintError):
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_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address)
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_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address)
if encumbered_sweeptx:
self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx.to_json())
encumbered_sweeptxs = [(None, maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address))]
for prev_txid, encumbered_tx in encumbered_sweeptxs:
if prev_txid is None:
prev_txid = ctx.txid()
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:
@ -739,7 +749,7 @@ def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey)
return EncumberedTransaction(sweep_tx, csv_delay=0)
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,
@ -766,11 +776,11 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret
witness_script=witness_script,
privkey=revocation_privkey,
is_revocation=True)
return EncumberedTransaction(sweep_tx, csv_delay=0)
return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0)
def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]:
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)
@ -782,21 +792,77 @@ def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
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
else:
return None
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)
return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay)
# 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:

18
electrum/lnutil.py

@ -173,8 +173,8 @@ def derive_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:
def derive_privkey(secret: int, per_commitment_point: bytes) -> int:
assert type(secret) is int
basepoint = secret_to_pubkey(secret)
basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint))
basepoint_bytes = secret_to_pubkey(secret)
basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint_bytes))
basepoint %= CURVE_ORDER
return basepoint
@ -212,7 +212,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela
final_amount_sat = (amount_msat - fee) // 1000
assert final_amount_sat > 0, final_amount_sat
output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat)
return output
return script, output
def make_htlc_tx_witness(remotehtlcsig, localhtlcsig, payment_preimage, witness_script):
assert type(remotehtlcsig) is bytes
@ -296,7 +296,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc):
# HTLC-success for the HTLC spending from a received HTLC output
# if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success
is_htlc_success = for_us == we_receive
htlc_tx_output = make_htlc_tx_output(
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,
@ -317,7 +317,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc):
if is_htlc_success:
cltv_expiry = 0
htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
return htlc_tx
return script, htlc_tx
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
payment_basepoint: bytes, remote_payment_basepoint: bytes,
@ -598,12 +598,16 @@ def generate_keypair(ln_keystore: BIP32_KeyStore, key_family: LnKeyFamily, index
return Keypair(*ln_keystore.get_keypair([key_family, 0, index], None))
class EncumberedTransaction(NamedTuple("EncumberedTransaction", [('tx', Transaction),
('csv_delay', Optional[int])])):
class EncumberedTransaction(NamedTuple("EncumberedTransaction", [('name', str),
('tx', Transaction),
('csv_delay', int),
('cltv_expiry', int),])):
def to_json(self) -> dict:
return {
'name': self.name,
'tx': str(self.tx),
'csv_delay': self.csv_delay,
'cltv_expiry': self.cltv_expiry,
}
@classmethod

78
electrum/lnwatcher.py

@ -35,15 +35,15 @@ class LNWatcher(PrintError):
self.lock = threading.RLock()
self.watched_addresses = set()
self.channel_info = storage.get('channel_info', {}) # access with 'lock'
# TODO structure will need to change when we handle HTLCs......
# [funding_outpoint_str][ctx_txid] -> set of EncumberedTransaction
# [funding_outpoint_str][prev_txid] -> set of EncumberedTransaction
# prev_txid is the txid of a tx that is watched for confirmations
# access with 'lock'
self.sweepstore = defaultdict(lambda: defaultdict(set))
for funding_outpoint, ctxs in storage.get('sweepstore', {}).items():
for ctx_txid, set_of_txns in ctxs.items():
for txid, set_of_txns in ctxs.items():
for e_tx in set_of_txns:
e_tx2 = EncumberedTransaction.from_json(e_tx)
self.sweepstore[funding_outpoint][ctx_txid].add(e_tx2)
self.sweepstore[funding_outpoint][txid].add(e_tx2)
self.network.register_callback(self.on_network_update,
['network_updated', 'blockchain_updated', 'verified', 'wallet_updated'])
@ -85,8 +85,8 @@ class LNWatcher(PrintError):
sweepstore = {}
for funding_outpoint, ctxs in self.sweepstore.items():
sweepstore[funding_outpoint] = {}
for ctx_txid, set_of_txns in ctxs.items():
sweepstore[funding_outpoint][ctx_txid] = [e_tx.to_json() for e_tx in set_of_txns]
for prev_txid, set_of_txns in ctxs.items():
sweepstore[funding_outpoint][prev_txid] = [e_tx.to_json() for e_tx in set_of_txns]
storage.put('sweepstore', sweepstore)
storage.write()
@ -134,7 +134,7 @@ class LNWatcher(PrintError):
# only care about confirmed and verified ctxs. TODO is this necessary?
if conf == 0:
return
keep_watching_this = await self.inspect_ctx_candidate(funding_outpoint, ctx_candidate)
keep_watching_this = await self.inspect_tx_candidate(funding_outpoint, ctx_candidate)
if not keep_watching_this:
self.stop_and_delete(funding_outpoint)
@ -142,55 +142,77 @@ class LNWatcher(PrintError):
# TODO delete channel from watcher_db
pass
async def inspect_ctx_candidate(self, funding_outpoint, ctx):
async def inspect_tx_candidate(self, funding_outpoint, prev_tx):
"""Returns True iff found any not-deeply-spent outputs that we could
potentially sweep at some point."""
# make sure we are subscribed to all outputs of ctx
# make sure we are subscribed to all outputs of tx
not_yet_watching = False
for o in ctx.outputs():
for o in prev_tx.outputs():
if o.address not in self.watched_addresses:
self.watch_address(o.address)
not_yet_watching = True
if not_yet_watching:
return True
# get all possible responses we have
ctx_txid = ctx.txid()
prev_txid = prev_tx.txid()
with self.lock:
encumbered_sweep_txns = self.sweepstore[funding_outpoint][ctx_txid]
encumbered_sweep_txns = self.sweepstore[funding_outpoint][prev_txid]
if len(encumbered_sweep_txns) == 0:
# no useful response for this channel close..
if self.get_tx_mined_status(ctx_txid) == TX_MINED_STATUS_DEEP:
self.print_error("channel close detected for {}. but can't sweep anything :(".format(funding_outpoint))
if self.get_tx_mined_status(prev_txid) == TX_MINED_STATUS_DEEP:
return False
# check if any response applies
keep_watching_this = False
local_height = self.network.get_local_height()
for e_tx in encumbered_sweep_txns:
txs_to_add = []
for e_tx in list(encumbered_sweep_txns):
conflicts = self.addr_sync.get_conflicting_transactions(e_tx.tx.txid(), e_tx.tx, include_self=True)
conflict_mined_status = self.get_deepest_tx_mined_status_for_txids(conflicts)
if conflict_mined_status != TX_MINED_STATUS_DEEP:
keep_watching_this = True
if conflict_mined_status == TX_MINED_STATUS_FREE:
tx_height = self.addr_sync.get_tx_height(ctx_txid).height
tx_height = self.addr_sync.get_tx_height(prev_txid).height
num_conf = local_height - tx_height + 1
if num_conf >= e_tx.csv_delay:
try:
await self.network.broadcast_transaction(e_tx.tx)
except Exception as e:
self.print_error('broadcast: {}, {}'.format('failure', repr(e)))
broadcast = True
if e_tx.cltv_expiry:
if local_height > e_tx.cltv_expiry:
self.print_error('CLTV ({} > {}) fulfilled'.format(local_height, e_tx.cltv_expiry))
else:
self.print_error('broadcast: {}'.format('success'))
else:
self.print_error('waiting for CSV ({} < {}) for funding outpoint {} and ctx {}'
.format(num_conf, e_tx.csv_delay, funding_outpoint, ctx.txid()))
self.print_error('waiting for CLTV ({} > {}) for funding outpoint {} and tx {}'
.format(local_height, e_tx.cltv_expiry, funding_outpoint, prev_tx.txid()))
broadcast = False
if e_tx.csv_delay:
if num_conf < e_tx.csv_delay:
self.print_error('waiting for CSV ({} >= {}) for funding outpoint {} and tx {}'
.format(num_conf, e_tx.csv_delay, funding_outpoint, prev_tx.txid()))
broadcast = False
if broadcast:
await self.broadcast_or_log(e_tx)
else:
# not mined or in mempool
keep_watching_this |= await self.inspect_tx_candidate(funding_outpoint, e_tx.tx)
return keep_watching_this
async def broadcast_or_log(self, e_tx):
try:
txid = await self.network.broadcast_transaction(e_tx.tx)
except Exception as e:
self.print_error(f'broadcast: {e_tx.name}: failure: {repr(e)}')
else:
self.print_error(f'broadcast: {e_tx.name}: success. txid: {txid}')
return True
return False
@with_watchtower
def add_sweep_tx(self, funding_outpoint: str, ctx_txid: str, sweeptx):
def add_sweep_tx(self, funding_outpoint: str, prev_txid: str, sweeptx):
encumbered_sweeptx = EncumberedTransaction.from_json(sweeptx)
with self.lock:
self.sweepstore[funding_outpoint][ctx_txid].add(encumbered_sweeptx)
tx_set = self.sweepstore[funding_outpoint][prev_txid]
if encumbered_sweeptx in tx_set:
return False
tx_set.add(encumbered_sweeptx)
self.write_to_disk()
return True
def get_tx_mined_status(self, txid: str):
if not txid:

Loading…
Cancel
Save