lnsweep: update sweeps to_remote and htlcs
* sweep to_remote output, as this is now a p2wsh (previously internal wallet address) * sweep htlc outputs with new scripts
This commit is contained in:
@@ -18,7 +18,7 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
|
|||||||
LOCAL, REMOTE, make_htlc_output_witness_script,
|
LOCAL, REMOTE, make_htlc_output_witness_script,
|
||||||
get_ordered_channel_configs, get_per_commitment_secret_from_seed,
|
get_ordered_channel_configs, get_per_commitment_secret_from_seed,
|
||||||
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
|
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
|
||||||
map_htlcs_to_ctx_output_idxs, Direction)
|
map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script)
|
||||||
from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput,
|
from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput,
|
||||||
PartialTxOutput, TxOutpoint)
|
PartialTxOutput, TxOutpoint)
|
||||||
from .simple_config import SimpleConfig
|
from .simple_config import SimpleConfig
|
||||||
@@ -147,7 +147,7 @@ def create_sweeptx_for_their_revoked_htlc(
|
|||||||
htlc_tx: Transaction,
|
htlc_tx: Transaction,
|
||||||
sweep_address: str) -> Optional[SweepInfo]:
|
sweep_address: str) -> Optional[SweepInfo]:
|
||||||
|
|
||||||
x = analyze_ctx(chan, ctx)
|
x = extract_ctx_secrets(chan, ctx)
|
||||||
if not x:
|
if not x:
|
||||||
return
|
return
|
||||||
ctn, their_pcp, is_revocation, per_commitment_secret = x
|
ctn, their_pcp, is_revocation, per_commitment_secret = x
|
||||||
@@ -187,10 +187,18 @@ def create_sweeptxs_for_our_ctx(
|
|||||||
*, chan: 'AbstractChannel',
|
*, chan: 'AbstractChannel',
|
||||||
ctx: Transaction,
|
ctx: Transaction,
|
||||||
sweep_address: str) -> Optional[Dict[str, SweepInfo]]:
|
sweep_address: str) -> Optional[Dict[str, SweepInfo]]:
|
||||||
"""Handle the case where we force close unilaterally with our latest ctx.
|
"""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).
|
|
||||||
|
We sweep:
|
||||||
|
to_local: CSV delayed
|
||||||
|
htlc success: CSV delay with anchors, no delay otherwise
|
||||||
|
htlc timeout: CSV delay with anchors, CLTV locktime
|
||||||
|
second-stage htlc transactions: CSV delay
|
||||||
|
|
||||||
'to_local' can be swept even if this is a breach (by us),
|
'to_local' can be swept even if this is a breach (by us),
|
||||||
but HTLCs cannot (old HTLCs are no longer stored).
|
but HTLCs cannot (old HTLCs are no longer stored).
|
||||||
|
|
||||||
|
Outputs with CSV/CLTV are redeemed by LNWatcher.
|
||||||
"""
|
"""
|
||||||
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
|
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
|
||||||
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
||||||
@@ -315,7 +323,7 @@ def create_sweeptxs_for_our_ctx(
|
|||||||
return txs
|
return txs
|
||||||
|
|
||||||
|
|
||||||
def analyze_ctx(chan: 'Channel', ctx: Transaction):
|
def extract_ctx_secrets(chan: 'Channel', ctx: Transaction):
|
||||||
# note: the remote sometimes has two valid non-revoked commitment transactions,
|
# note: the remote sometimes has two valid non-revoked commitment transactions,
|
||||||
# either of which could be broadcast
|
# either of which could be broadcast
|
||||||
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
||||||
@@ -351,14 +359,23 @@ def create_sweeptxs_for_their_ctx(
|
|||||||
"""Handle the case when the remote force-closes with their ctx.
|
"""Handle the case when the remote force-closes with their ctx.
|
||||||
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
|
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
|
||||||
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
|
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
|
||||||
|
|
||||||
|
We sweep:
|
||||||
|
to_local: if revoked
|
||||||
|
to_remote: CSV delay with anchors, otherwise sweeping not needed
|
||||||
|
htlc success: CSV delay with anchors, no delay otherwise, or revoked
|
||||||
|
htlc timeout: CSV delay with anchors, CLTV locktime, or revoked
|
||||||
|
second-stage htlc transactions: CSV delay
|
||||||
|
|
||||||
|
Outputs with CSV/CLTV are redeemed by LNWatcher.
|
||||||
"""
|
"""
|
||||||
txs = {} # type: Dict[str, SweepInfo]
|
txs = {} # type: Dict[str, SweepInfo]
|
||||||
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
||||||
x = analyze_ctx(chan, ctx)
|
x = extract_ctx_secrets(chan, ctx)
|
||||||
if not x:
|
if not x:
|
||||||
return
|
return
|
||||||
ctn, their_pcp, is_revocation, per_commitment_secret = x
|
ctn, their_pcp, is_revocation, per_commitment_secret = x
|
||||||
# to_local and to_remote addresses
|
# to_local
|
||||||
our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)
|
our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)
|
||||||
their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)
|
their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)
|
||||||
witness_script = make_commitment_output_to_local_witness_script(
|
witness_script = make_commitment_output_to_local_witness_script(
|
||||||
@@ -377,6 +394,7 @@ def create_sweeptxs_for_their_ctx(
|
|||||||
if not found_to_local and not found_to_remote:
|
if not found_to_local and not found_to_remote:
|
||||||
return
|
return
|
||||||
chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}')
|
chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}')
|
||||||
|
# to_local is handled by lnwatcher
|
||||||
if is_revocation:
|
if is_revocation:
|
||||||
our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret)
|
our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret)
|
||||||
gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, sweep_address)
|
gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, sweep_address)
|
||||||
@@ -387,12 +405,42 @@ def create_sweeptxs_for_their_ctx(
|
|||||||
csv_delay=0,
|
csv_delay=0,
|
||||||
cltv_abs=0,
|
cltv_abs=0,
|
||||||
gen_tx=gen_tx)
|
gen_tx=gen_tx)
|
||||||
# prep
|
|
||||||
|
# to_remote
|
||||||
|
if chan.has_anchors():
|
||||||
|
csv_delay = 1
|
||||||
|
sweep_to_remote = True
|
||||||
|
our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey)
|
||||||
|
else:
|
||||||
|
assert chan.is_static_remotekey_enabled()
|
||||||
|
csv_delay = 0
|
||||||
|
sweep_to_remote = False
|
||||||
|
our_payment_privkey = None
|
||||||
|
|
||||||
|
if sweep_to_remote:
|
||||||
|
assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)
|
||||||
|
output_idxs = ctx.get_output_idxs_from_address(to_remote_address)
|
||||||
|
if output_idxs:
|
||||||
|
output_idx = output_idxs.pop()
|
||||||
|
prevout = ctx.txid() + ':%d' % output_idx
|
||||||
|
sweep_tx = lambda: create_sweeptx_their_ctx_to_remote(
|
||||||
|
sweep_address=sweep_address,
|
||||||
|
ctx=ctx,
|
||||||
|
output_idx=output_idx,
|
||||||
|
our_payment_privkey=our_payment_privkey,
|
||||||
|
config=chan.lnworker.config,
|
||||||
|
has_anchors=chan.has_anchors()
|
||||||
|
)
|
||||||
|
txs[prevout] = SweepInfo(
|
||||||
|
name='their_ctx_to_remote',
|
||||||
|
csv_delay=csv_delay,
|
||||||
|
cltv_abs=0,
|
||||||
|
gen_tx=sweep_tx)
|
||||||
|
|
||||||
|
# HTLCs
|
||||||
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)
|
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)
|
||||||
our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey)
|
our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey)
|
||||||
their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp)
|
their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp)
|
||||||
# to_local is handled by lnwatcher
|
|
||||||
# HTLCs
|
|
||||||
def create_sweeptx_for_htlc(
|
def create_sweeptx_for_htlc(
|
||||||
*, htlc: 'UpdateAddHtlc',
|
*, htlc: 'UpdateAddHtlc',
|
||||||
is_received_htlc: bool,
|
is_received_htlc: bool,
|
||||||
@@ -408,6 +456,7 @@ def create_sweeptxs_for_their_ctx(
|
|||||||
has_anchors=chan.has_anchors())
|
has_anchors=chan.has_anchors())
|
||||||
|
|
||||||
cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0
|
cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0
|
||||||
|
csv_delay = 1 if chan.has_anchors() else 0
|
||||||
prevout = ctx.txid() + ':%d'%ctx_output_idx
|
prevout = ctx.txid() + ':%d'%ctx_output_idx
|
||||||
sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
|
sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
@@ -418,10 +467,12 @@ def create_sweeptxs_for_their_ctx(
|
|||||||
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
|
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
|
||||||
is_revocation=is_revocation,
|
is_revocation=is_revocation,
|
||||||
cltv_abs=cltv_abs,
|
cltv_abs=cltv_abs,
|
||||||
config=chan.lnworker.config)
|
config=chan.lnworker.config,
|
||||||
|
has_anchors=chan.has_anchors(),
|
||||||
|
)
|
||||||
txs[prevout] = SweepInfo(
|
txs[prevout] = SweepInfo(
|
||||||
name=f'their_ctx_htlc_{ctx_output_idx}',
|
name=f'their_ctx_htlc_{ctx_output_idx}{"_for_revoked_ctx" if is_revocation else ""}',
|
||||||
csv_delay=0,
|
csv_delay=csv_delay,
|
||||||
cltv_abs=cltv_abs,
|
cltv_abs=cltv_abs,
|
||||||
gen_tx=sweep_tx)
|
gen_tx=sweep_tx)
|
||||||
# received HTLCs, in their ctx --> "timeout"
|
# received HTLCs, in their ctx --> "timeout"
|
||||||
@@ -488,7 +539,10 @@ def create_sweeptx_their_ctx_htlc(
|
|||||||
ctx: Transaction, witness_script: bytes, sweep_address: str,
|
ctx: Transaction, witness_script: bytes, sweep_address: str,
|
||||||
preimage: Optional[bytes], output_idx: int,
|
preimage: Optional[bytes], output_idx: int,
|
||||||
privkey: bytes, is_revocation: bool,
|
privkey: bytes, is_revocation: bool,
|
||||||
cltv_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]:
|
cltv_abs: int,
|
||||||
|
config: SimpleConfig,
|
||||||
|
has_anchors: bool,
|
||||||
|
) -> Optional[PartialTransaction]:
|
||||||
assert type(cltv_abs) is int
|
assert type(cltv_abs) is int
|
||||||
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
|
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
|
||||||
val = ctx.outputs()[output_idx].value
|
val = ctx.outputs()[output_idx].value
|
||||||
@@ -497,6 +551,8 @@ def create_sweeptx_their_ctx_htlc(
|
|||||||
txin._trusted_value_sats = val
|
txin._trusted_value_sats = val
|
||||||
txin.witness_script = witness_script
|
txin.witness_script = witness_script
|
||||||
txin.script_sig = b''
|
txin.script_sig = b''
|
||||||
|
if has_anchors:
|
||||||
|
txin.nsequence = 1
|
||||||
sweep_inputs = [txin]
|
sweep_inputs = [txin]
|
||||||
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation)
|
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation)
|
||||||
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
|
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
|
||||||
@@ -518,7 +574,9 @@ def create_sweeptx_their_ctx_htlc(
|
|||||||
def create_sweeptx_their_ctx_to_remote(
|
def create_sweeptx_their_ctx_to_remote(
|
||||||
sweep_address: str, ctx: Transaction, output_idx: int,
|
sweep_address: str, ctx: Transaction, output_idx: int,
|
||||||
our_payment_privkey: ecc.ECPrivkey,
|
our_payment_privkey: ecc.ECPrivkey,
|
||||||
config: SimpleConfig) -> Optional[PartialTransaction]:
|
config: SimpleConfig,
|
||||||
|
has_anchors: bool,
|
||||||
|
) -> Optional[PartialTransaction]:
|
||||||
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
|
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
|
||||||
val = ctx.outputs()[output_idx].value
|
val = ctx.outputs()[output_idx].value
|
||||||
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
|
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
|
||||||
@@ -526,15 +584,31 @@ def create_sweeptx_their_ctx_to_remote(
|
|||||||
txin._trusted_value_sats = val
|
txin._trusted_value_sats = val
|
||||||
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh')
|
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh')
|
||||||
txin.script_descriptor = desc
|
txin.script_descriptor = desc
|
||||||
|
txin.pubkeys = [bfh(our_payment_pubkey)]
|
||||||
|
txin.num_sig = 1
|
||||||
|
if not has_anchors:
|
||||||
|
txin.script_type = 'p2wpkh'
|
||||||
|
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
|
||||||
|
else:
|
||||||
|
txin.script_sig = b''
|
||||||
|
txin.witness_script = make_commitment_output_to_remote_witness_script(bfh(our_payment_pubkey))
|
||||||
|
txin.nsequence = 1
|
||||||
|
tx_size_bytes = 196 # approx size of p2wsh->p2wpkh
|
||||||
sweep_inputs = [txin]
|
sweep_inputs = [txin]
|
||||||
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
|
|
||||||
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
|
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
|
||||||
outvalue = val - fee
|
outvalue = val - fee
|
||||||
if outvalue <= dust_threshold(): return None
|
if outvalue <= dust_threshold(): return None
|
||||||
sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
|
sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
|
||||||
sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs)
|
sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs)
|
||||||
sweep_tx.set_rbf(True)
|
|
||||||
sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()})
|
if not has_anchors:
|
||||||
|
sweep_tx.set_rbf(True)
|
||||||
|
sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()})
|
||||||
|
else:
|
||||||
|
sig = sweep_tx.sign_txin(0, our_payment_privkey.get_secret_bytes())
|
||||||
|
witness = construct_witness([sig, sweep_tx.inputs()[0].witness_script])
|
||||||
|
sweep_tx.inputs()[0].witness = bfh(witness)
|
||||||
|
|
||||||
if not sweep_tx.is_complete():
|
if not sweep_tx.is_complete():
|
||||||
raise Exception('channel close sweep tx is not complete')
|
raise Exception('channel close sweep tx is not complete')
|
||||||
return sweep_tx
|
return sweep_tx
|
||||||
|
|||||||
Reference in New Issue
Block a user