1
0

create sweep transaction outside of lnwatcher

This commit is contained in:
ThomasV
2018-10-05 19:37:55 +02:00
parent 72eb179c7a
commit 11c3ca281c
5 changed files with 201 additions and 225 deletions

View File

@@ -1,20 +1,11 @@
import threading
from typing import Optional, NamedTuple, Iterable
from typing import NamedTuple, Iterable
import os
from collections import defaultdict
from .util import PrintError, bh2u, bfh, NoDynamicFeeEstimates, aiosafe
from .lnutil import (extract_ctn_from_tx_and_chan, derive_privkey,
get_per_commitment_secret_from_seed, derive_pubkey,
make_commitment_output_to_remote_address,
RevocationStore, Outpoint)
from . import lnutil
from .bitcoin import redeem_script_to_address, TYPE_ADDRESS
from . import transaction
from .transaction import Transaction, TxOutput
from . import ecc
from .lnutil import EncumberedTransaction, Outpoint
from . import wallet
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
from .storage import WalletStorage
from .address_synchronizer import AddressSynchronizer
@@ -22,23 +13,9 @@ from .address_synchronizer import AddressSynchronizer
TX_MINED_STATUS_DEEP, TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL, TX_MINED_STATUS_FREE = range(0, 4)
class EncumberedTransaction(NamedTuple("EncumberedTransaction", [('tx', Transaction),
('csv_delay', Optional[int])])):
def to_json(self) -> dict:
return {
'tx': str(self.tx),
'csv_delay': self.csv_delay,
}
@classmethod
def from_json(cls, d: dict):
d2 = dict(d)
d2['tx'] = Transaction(d['tx'])
return EncumberedTransaction(**d2)
class ChannelWatchInfo(NamedTuple("ChannelWatchInfo", [('outpoint', Outpoint),
('sweep_address', str),
('local_pubkey', bytes),
('remote_pubkey', bytes),
('last_ctn_our_ctx', int),
@@ -47,7 +24,6 @@ class ChannelWatchInfo(NamedTuple("ChannelWatchInfo", [('outpoint', Outpoint),
def to_json(self) -> dict:
return {
'outpoint': self.outpoint,
'sweep_address': self.sweep_address,
'local_pubkey': bh2u(self.local_pubkey),
'remote_pubkey': bh2u(self.remote_pubkey),
'last_ctn_our_ctx': self.last_ctn_our_ctx,
@@ -112,13 +88,12 @@ class LNWatcher(PrintError):
storage.put('sweepstore', sweepstore)
storage.write()
def watch_channel(self, chan, sweep_address, callback_funding_txo_spent):
def watch_channel(self, chan, callback_funding_txo_spent):
address = chan.get_funding_address()
self.watch_address(address)
with self.lock:
if address not in self.channel_info:
self.channel_info[address] = ChannelWatchInfo(outpoint=chan.funding_outpoint,
sweep_address=sweep_address,
local_pubkey=chan.local_config.payment_basepoint.pubkey,
remote_pubkey=chan.remote_config.payment_basepoint.pubkey,
last_ctn_our_ctx=0,
@@ -212,17 +187,6 @@ class LNWatcher(PrintError):
.format(num_conf, e_tx.csv_delay, funding_outpoint, ctx.txid()))
return keep_watching_this
def _get_sweep_address_for_chan(self, chan) -> str:
funding_address = chan.get_funding_address()
try:
channel_info = self.channel_info[funding_address]
except KeyError:
# this is used during channel opening, as we only start watching
# the channel once it gets into the "opening" state, but we need to
# process the first ctx before that.
return chan.sweep_address
return channel_info.sweep_address
def _get_last_ctn_for_processed_ctx(self, funding_address: str, ours: bool) -> int:
try:
ci = self.channel_info[funding_address]
@@ -259,45 +223,19 @@ class LNWatcher(PrintError):
ci = ci._replace(last_ctn_revoked_pcs=ci.last_ctn_revoked_pcs + 1)
self.channel_info[funding_address] = ci
# TODO batch sweeps
# TODO sweep HTLC outputs
def process_new_offchain_ctx(self, chan, ctx, ours: bool):
funding_address = chan.get_funding_address()
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
latest_ctn_on_channel = chan.local_state.ctn if ours else chan.remote_state.ctn
def add_offchain_ctx(self, ctn, funding_address, ours, outpoint, ctx_id, encumbered_sweeptx):
last_ctn_watcher_saw = self._get_last_ctn_for_processed_ctx(funding_address, ours)
if latest_ctn_on_channel + 1 != ctn:
raise Exception('unexpected ctn {}. latest is {}. our ctx: {}'.format(ctn, latest_ctn_on_channel, ours))
if last_ctn_watcher_saw + 1 != ctn:
raise Exception('watcher skipping ctns!! ctn {}. last seen {}. our ctx: {}'.format(ctn, last_ctn_watcher_saw, ours))
#self.print_error("process_new_offchain_ctx. funding {}, ours {}, ctn {}, ctx {}"
# .format(chan.funding_outpoint.to_str(), ours, ctn, ctx.txid()))
sweep_address = self._get_sweep_address_for_chan(chan)
if ours:
our_per_commitment_secret = get_per_commitment_secret_from_seed(
chan.local_state.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(chan, ctx, our_cur_pcp, sweep_address)
else:
their_cur_pcp = chan.remote_state.next_per_commitment_point
encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_cur_pcp, sweep_address)
self.add_to_sweepstore(chan.funding_outpoint.to_str(), ctx.txid(), encumbered_sweeptx)
self.add_to_sweepstore(outpoint, ctx_id, encumbered_sweeptx)
self._inc_last_ctn_for_processed_ctx(funding_address, ours)
self.write_to_disk()
def process_new_revocation_secret(self, chan, per_commitment_secret: bytes):
funding_address = chan.get_funding_address()
ctx = chan.remote_commitment_to_be_revoked
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
latest_ctn_on_channel = chan.remote_state.ctn
def add_revocation_secret(self, ctn, funding_address, outpoint, ctx_id, encumbered_sweeptx):
last_ctn_watcher_saw = self._get_last_ctn_for_revoked_secret(funding_address)
if latest_ctn_on_channel != ctn:
raise Exception('unexpected ctn {}. latest is {}'.format(ctn, latest_ctn_on_channel))
if last_ctn_watcher_saw + 1 != ctn:
raise Exception('watcher skipping ctns!! ctn {}. last seen {}'.format(ctn, last_ctn_watcher_saw))
sweep_address = self._get_sweep_address_for_chan(chan)
encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret, sweep_address)
self.add_to_sweepstore(chan.funding_outpoint.to_str(), ctx.txid(), encumbered_sweeptx)
self.add_to_sweepstore(outpoint, ctx_id, encumbered_sweeptx)
self._inc_last_ctn_for_revoked_secret(funding_address)
self.write_to_disk()
@@ -336,141 +274,3 @@ class LNWatcher(PrintError):
def print_tx_broadcast_result(self, res):
success, msg = res
self.print_error('broadcast: {}, {}'.format('success' if success else 'failure', msg))
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.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)
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(sweep_tx, csv_delay=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 = lnutil.derive_blinded_privkey(chan.local_config.revocation_basepoint.privkey,
per_commitment_secret)
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
to_self_delay = chan.local_config.to_self_delay
delayed_pubkey = derive_pubkey(chan.remote_config.delayed_basepoint.pubkey,
per_commitment_point)
witness_script = bh2u(lnutil.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(sweep_tx, csv_delay=0)
def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(our_pcp, bytes)
delayed_bp_privkey = ecc.ECPrivkey(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)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
revocation_pubkey = lnutil.derive_blinded_pubkey(chan.remote_config.revocation_basepoint.pubkey,
our_pcp)
to_self_delay = chan.remote_config.to_self_delay
witness_script = bh2u(lnutil.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)
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=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
to_self_delay=to_self_delay)
return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay)
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 = transaction.construct_witness([sig, int(is_revocation), witness_script])
sweep_tx.inputs()[0]['witness'] = witness
return sweep_tx