Browse Source

lnwatcher rebased

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
261fefb6f3
  1. 14
      electrum/address_synchronizer.py
  2. 2
      electrum/daemon.py
  3. 13
      electrum/lnbase.py
  4. 48
      electrum/lnhtlc.py
  5. 23
      electrum/lnutil.py
  6. 578
      electrum/lnwatcher.py
  7. 31
      electrum/lnworker.py
  8. 3
      electrum/tests/test_lnhtlc.py

14
electrum/address_synchronizer.py

@ -94,6 +94,9 @@ class AddressSynchronizer(Logger):
self.load_unverified_transactions()
self.remove_local_transactions_we_dont_have()
def synchronize(self):
pass
def is_mine(self, address):
return self.db.is_addr_in_history(address)
@ -173,11 +176,13 @@ class AddressSynchronizer(Logger):
if self.synchronizer:
self.synchronizer.add(address)
def get_conflicting_transactions(self, tx_hash, tx):
def get_conflicting_transactions(self, tx_hash, tx, include_self=False):
"""Returns a set of transaction hashes from the wallet history that are
directly conflicting with tx, i.e. they have common outpoints being
spent with tx. If the tx is already in wallet history, that will not be
reported as a conflict.
spent with tx.
include_self specifies whether the tx itself should be reported as a
conflict (if already in wallet history)
"""
conflicting_txns = set()
with self.transaction_lock:
@ -197,7 +202,8 @@ class AddressSynchronizer(Logger):
# this tx is already in history, so it conflicts with itself
if len(conflicting_txns) > 1:
raise Exception('Found conflicting transactions already in wallet history.')
conflicting_txns -= {tx_hash}
if not include_self:
conflicting_txns -= {tx_hash}
return conflicting_txns
def add_transaction(self, tx_hash, tx, allow_unrelated=False):

2
electrum/daemon.py

@ -45,6 +45,7 @@ from .simple_config import SimpleConfig
from .exchange_rate import FxThread
from .plugin import run_hook
from .logging import get_logger
from .lnworker import LNWorker
_logger = get_logger(__name__)
@ -260,6 +261,7 @@ class Daemon(DaemonThread):
return
wallet = Wallet(storage)
wallet.start_network(self.network)
wallet.lnworker = LNWorker(wallet, self.network)
self.wallets[path] = wallet
return wallet

13
electrum/lnbase.py

@ -277,6 +277,7 @@ class Peer(PrintError):
self.lnworker = lnworker
self.privkey = lnworker.privkey
self.network = lnworker.network
self.lnwatcher = lnworker.network.lnwatcher
self.channel_db = lnworker.network.channel_db
self.read_buffer = b''
self.ping_time = 0
@ -472,7 +473,7 @@ class Peer(PrintError):
self.network.trigger_callback('channel', chan)
@aiosafe
async def channel_establishment_flow(self, wallet, config, password, funding_sat, push_msat, temp_channel_id):
async def channel_establishment_flow(self, wallet, config, password, funding_sat, push_msat, temp_channel_id, sweep_address):
await self.initialized
# see lnd/keychain/derivation.go
keyfamilymultisig = 0
@ -583,9 +584,12 @@ class Peer(PrintError):
current_htlc_signatures = None,
feerate=local_feerate
),
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth)
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
"remote_commitment_to_be_revoked": None,
}
m = HTLCStateMachine(chan)
m.lnwatcher = self.lnwatcher
m.sweep_address = sweep_address
sig_64, _ = m.sign_next_commitment()
self.send_message(gen_msg("funding_created",
temporary_channel_id=temp_channel_id,
@ -599,6 +603,7 @@ class Peer(PrintError):
# broadcast funding tx
success, _txid = await self.network.broadcast_transaction(funding_tx)
assert success, success
m.remote_commitment_to_be_revoked = m.pending_remote_commitment
m.remote_state = m.remote_state._replace(ctn=0)
m.local_state = m.local_state._replace(ctn=0, current_commitment_signature=remote_sig)
m.set_state('OPENING')
@ -890,8 +895,10 @@ class Peer(PrintError):
chan.receive_htlc_settle(preimage, int.from_bytes(update_fulfill_htlc_msg["id"], "big"))
await self.receive_commitment(chan)
self.revoke(chan)
# FIXME why is this not using the HTLC state machine?
bare_ctx = chan.make_commitment(chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point,
msat_remote, msat_local)
self.lnwatcher.process_new_offchain_ctx(chan, bare_ctx, ours=False)
sig_64 = sign_and_get_sig_string(bare_ctx, chan.local_config, chan.remote_config)
res = bh2u(preimage)
payment_succeeded = True
@ -958,8 +965,10 @@ class Peer(PrintError):
self.send_message(gen_msg("update_fulfill_htlc", channel_id=channel_id, id=htlc_id, payment_preimage=payment_preimage))
# remote commitment transaction without htlcs
# FIXME why is this not using the HTLC state machine?
bare_ctx = chan.make_commitment(chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point,
chan.remote_state.amount_msat - expected_received_msat, chan.local_state.amount_msat + expected_received_msat)
self.lnwatcher.process_new_offchain_ctx(chan, bare_ctx, ours=False)
sig_64 = sign_and_get_sig_string(bare_ctx, chan.local_config, chan.remote_config)
self.send_message(gen_msg("commitment_signed", channel_id=channel_id, signature=sig_64, num_htlcs=0))

48
electrum/lnhtlc.py

@ -2,6 +2,7 @@
from collections import namedtuple
import binascii
import json
from .util import bfh, PrintError, bh2u
from .bitcoin import Hash
from .bitcoin import redeem_script_to_address
@ -13,7 +14,9 @@ from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blin
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
from .lnutil import funding_output_script, extract_ctn_from_tx_and_chan
from .transaction import Transaction
SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
@ -126,6 +129,11 @@ class HTLCStateMachine(PrintError):
self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"]
self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"]
# FIXME this is a tx serialised in the custom electrum partial tx format.
# we should not persist txns in this format. we should persist htlcs, and be able to derive
# any past commitment transaction and use that instead; until then...
self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"])
self.local_update_log = []
self.remote_update_log = []
@ -141,6 +149,8 @@ class HTLCStateMachine(PrintError):
self._is_funding_txo_spent = None # "don't know"
self.set_state('DISCONNECTED')
self.lnwatcher = None
def set_state(self, state: str):
self._state = state
@ -203,7 +213,8 @@ class HTLCStateMachine(PrintError):
if htlc.l_locked_in is None: htlc.l_locked_in = self.local_state.ctn
self.print_error("sign_next_commitment")
sig_64 = sign_and_get_sig_string(self.pending_remote_commitment, self.local_config, self.remote_config)
pending_remote_commitment = self.pending_remote_commitment
sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.local_config, self.remote_config)
their_remote_htlc_privkey_number = derive_privkey(
int.from_bytes(self.local_config.htlc_basepoint.privkey, 'big'),
@ -224,7 +235,7 @@ class HTLCStateMachine(PrintError):
print("value too small, skipping. htlc amt: {}, weight: {}, remote feerate {}, remote dust limit {}".format( htlc.amount_msat, weight, feerate, self.remote_config.dust_limit_sat))
continue
original_htlc_output_index = 0
args = [self.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash, self.pending_remote_commitment, original_htlc_output_index]
args = [self.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash, pending_remote_commitment, original_htlc_output_index]
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])
@ -236,6 +247,9 @@ class HTLCStateMachine(PrintError):
if self.constraints.is_initiator and (self.pending_fee.progress & FUNDEE_ACKED):
self.pending_fee.progress |= FUNDER_SIGNED
if self.lnwatcher:
self.lnwatcher.process_new_offchain_ctx(self, pending_remote_commitment, ours=False)
return sig_64, htlcsigs
def receive_new_commitment(self, sig, htlc_sigs):
@ -256,20 +270,21 @@ class HTLCStateMachine(PrintError):
if htlc.r_locked_in is None: htlc.r_locked_in = self.remote_state.ctn
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
preimage_hex = self.pending_local_commitment.serialize_preimage(0)
pending_local_commitment = self.pending_local_commitment
preimage_hex = pending_local_commitment.serialize_preimage(0)
pre_hash = Hash(bfh(preimage_hex))
if not ecc.verify_signature(self.remote_config.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
if len(self.htlcs_in_remote) > 0 and len(self.pending_local_commitment.outputs()) == 3:
if len(self.htlcs_in_remote) > 0 and len(pending_local_commitment.outputs()) == 3:
print("CHECKING HTLC SIGS")
we_receive = True
payment_hash = self.htlcs_in_remote[0].payment_hash
amount_msat = self.htlcs_in_remote[0].amount_msat
cltv_expiry = self.htlcs_in_remote[0].cltv_expiry
htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, amount_msat, cltv_expiry, payment_hash, self.pending_local_commitment, 0)
htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, amount_msat, cltv_expiry, payment_hash, pending_local_commitment, 0)
pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.remote_config.htlc_basepoint.pubkey, this_point)
if not ecc.verify_signature(remote_htlc_pubkey, htlc_sigs[0], pre_hash):
@ -283,6 +298,9 @@ class HTLCStateMachine(PrintError):
if self.constraints.is_initiator and (self.pending_fee.progress & FUNDEE_ACKED):
self.pending_fee.progress |= FUNDER_SIGNED
if self.lnwatcher:
self.lnwatcher.process_new_offchain_ctx(self, pending_local_commitment, ours=True)
def revoke_current_commitment(self):
"""
@ -350,6 +368,20 @@ class HTLCStateMachine(PrintError):
"""
self.print_error("receive_revocation")
cur_point = self.remote_state.current_per_commitment_point
derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True)
if cur_point != derived_point:
raise Exception('revoked secret not for current point')
# 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
prev_remote_commitment = self.pending_remote_commitment
self.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
if self.lnwatcher:
self.lnwatcher.process_new_revocation_secret(self, revocation.per_commitment_secret)
settle_fails2 = []
for x in self.remote_update_log:
if type(x) is not SettleHtlc:
@ -386,8 +418,6 @@ class HTLCStateMachine(PrintError):
self.total_msat_received += received_this_batch
self.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
next_point = self.remote_state.next_per_commitment_point
print("RECEIVED", received_this_batch)
@ -408,6 +438,7 @@ class HTLCStateMachine(PrintError):
self.local_commitment = self.pending_local_commitment
self.remote_commitment = self.pending_remote_commitment
self.remote_commitment_to_be_revoked = prev_remote_commitment
@staticmethod
def htlcsum(htlcs):
@ -574,6 +605,7 @@ class HTLCStateMachine(PrintError):
"constraints": self.constraints,
"funding_outpoint": self.funding_outpoint,
"node_id": self.node_id,
"remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked),
}
def serialize(self):

23
electrum/lnutil.py

@ -1,7 +1,9 @@
from .util import bfh, bh2u, inv_dict
from .crypto import sha256
import json
from collections import namedtuple
from typing import NamedTuple
from .util import bfh, bh2u, inv_dict
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
@ -13,7 +15,6 @@ HTLC_TIMEOUT_WEIGHT = 663
HTLC_SUCCESS_WEIGHT = 703
Keypair = namedtuple("Keypair", ["pubkey", "privkey"])
Outpoint = namedtuple("Outpoint", ["txid", "output_index"])
ChannelConfig = namedtuple("ChannelConfig", [
"payment_basepoint", "multisig_key", "htlc_basepoint", "delayed_basepoint", "revocation_basepoint",
"to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs"])
@ -23,6 +24,10 @@ LocalState = namedtuple("LocalState", ["ctn", "per_commitment_secret_seed", "amo
ChannelConstraints = namedtuple("ChannelConstraints", ["capacity", "is_initiator", "funding_txn_minimum_depth"])
#OpenChannel = namedtuple("OpenChannel", ["channel_id", "short_channel_id", "funding_outpoint", "local_config", "remote_config", "remote_state", "local_state", "constraints", "node_id"])
class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
def to_str(self):
return "{}:{}".format(self.txid, self.output_index)
class UnableToDeriveSecret(Exception): pass
@ -366,17 +371,25 @@ def invert_short_channel_id(short_channel_id: bytes) -> (int, int, int):
oi = int.from_bytes(short_channel_id[6:8], byteorder='big')
return bh, tpos, oi
def get_obscured_ctn(ctn, local, remote):
def get_obscured_ctn(ctn: int, local: bytes, remote: bytes) -> int:
mask = int.from_bytes(sha256(local + remote)[-6:], 'big')
return ctn ^ mask
def extract_ctn_from_tx(tx, txin_index, local_payment_basepoint, remote_payment_basepoint):
def extract_ctn_from_tx(tx, txin_index: int, local_payment_basepoint: bytes,
remote_payment_basepoint: bytes) -> int:
tx.deserialize()
locktime = tx.locktime
sequence = tx.inputs()[txin_index]['sequence']
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
return get_obscured_ctn(obs, local_payment_basepoint, remote_payment_basepoint)
def extract_ctn_from_tx_and_chan(tx, chan) -> int:
local_pubkey = chan.local_config.payment_basepoint.pubkey
remote_pubkey = chan.remote_config.payment_basepoint.pubkey
return extract_ctn_from_tx(tx, txin_index=0,
local_payment_basepoint=local_pubkey,
remote_payment_basepoint=remote_pubkey)
def overall_weight(num_htlc):
return 500 + 172 * num_htlc + 224

578
electrum/lnwatcher.py

@ -1,151 +1,310 @@
import threading
import asyncio
from typing import Optional, NamedTuple, Iterable
import os
from collections import defaultdict
from .util import PrintError, bh2u, bfh, NoDynamicFeeEstimates, aiosafe
from .lnutil import (extract_ctn_from_tx, derive_privkey,
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, UnableToDeriveSecret)
RevocationStore, Outpoint)
from . import lnutil
from .bitcoin import redeem_script_to_address, TYPE_ADDRESS, address_to_scripthash
from .bitcoin import redeem_script_to_address, TYPE_ADDRESS
from . import transaction
from .transaction import Transaction, TxOutput
from . import ecc
from . import wallet
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
from .storage import WalletStorage
from .address_synchronizer import AddressSynchronizer
TX_MINED_STATUS_DEEP, TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL, TX_MINED_STATUS_FREE = range(0, 4)
TX_MINED_STATUS_DEEP, TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL, TX_MINED_STATUS_FREE = range(0, 4)
class LNWatcher(PrintError):
def __init__(self, network):
self.network = network
self.watched_channels = {}
self.address_status = {} # addr -> status
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,
}
@aiosafe
async def handle_addresses(self, funding_address):
queue = asyncio.Queue()
params = [address_to_scripthash(funding_address)]
await self.network.interface.session.subscribe('blockchain.scripthash.subscribe', params, queue)
await queue.get()
while True:
result = await queue.get()
await self.on_address_status(funding_address, result)
def watch_channel(self, chan, callback):
funding_address = chan.get_funding_address()
self.watched_channels[funding_address] = chan, callback
asyncio.get_event_loop().create_task(self.handle_addresses(funding_address))
@classmethod
def from_json(cls, d: dict):
d2 = dict(d)
d2['tx'] = Transaction(d['tx'])
return EncumberedTransaction(**d2)
async def on_address_status(self, addr, result):
if self.address_status.get(addr) != result:
self.address_status[addr] = result
result = await self.network.interface.session.send_request('blockchain.scripthash.listunspent', [address_to_scripthash(addr)])
chan, callback = self.watched_channels[addr]
await callback(chan, result)
class ChannelWatchInfo(NamedTuple("ChannelWatchInfo", [('outpoint', Outpoint),
('sweep_address', str),
('local_pubkey', bytes),
('remote_pubkey', bytes),
('last_ctn_our_ctx', int),
('last_ctn_their_ctx', int),
('last_ctn_revoked_pcs', int)])):
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,
'last_ctn_their_ctx': self.last_ctn_their_ctx,
'last_ctn_revoked_pcs': self.last_ctn_revoked_pcs,
}
@classmethod
def from_json(cls, d: dict):
d2 = dict(d)
d2['outpoint'] = Outpoint(*d['outpoint'])
d2['local_pubkey'] = bfh(d['local_pubkey'])
d2['remote_pubkey'] = bfh(d['remote_pubkey'])
return ChannelWatchInfo(**d2)
class LNChanCloseHandler(PrintError):
class LNWatcher(PrintError):
# TODO if verifier gets an incorrect merkle proof, that tx will never verify!!
# similarly, what if server ignores request for merkle proof?
# maybe we should disconnect from server in these cases
def __init__(self, network, wallet, chan):
def __init__(self, network):
self.network = network
self.wallet = wallet
self.sweep_address = wallet.get_receiving_address()
self.chan = chan
self.lock = threading.Lock()
self.funding_address = chan.get_funding_address()
path = os.path.join(network.config.path, "watcher_db")
storage = WalletStorage(path)
self.addr_sync = AddressSynchronizer(storage)
self.addr_sync.start_network(network)
self.lock = threading.RLock()
self.watched_addresses = set()
network.register_callback(self.on_network_update, ['updated'])
self.watch_address(self.funding_address)
async def on_network_update(self, event, *args):
if self.wallet.synchronizer.is_up_to_date():
await self.check_onchain_situation()
self.channel_info = {k: ChannelWatchInfo.from_json(v)
for k,v in storage.get('channel_info', {}).items()} # access with 'lock'
self.funding_txo_spent_callback = {} # funding_outpoint -> callback
# TODO structure will need to change when we handle HTLCs......
# [funding_outpoint_str][ctx_txid] -> set of EncumberedTransaction
# 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 e_tx in set_of_txns:
e_tx2 = EncumberedTransaction.from_json(e_tx)
self.sweepstore[funding_outpoint][ctx_txid].add(e_tx2)
self.network.register_callback(self.on_network_update, ['updated'])
def write_to_disk(self):
# FIXME: json => every update takes linear instead of constant disk write
with self.lock:
storage = self.addr_sync.storage
# self.channel_info
channel_info = {k: v.to_json() for k,v in self.channel_info.items()}
storage.put('channel_info', channel_info)
# self.sweepstore
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]
storage.put('sweepstore', sweepstore)
storage.write()
def stop_and_delete(self):
self.network.unregister_callback(self.on_network_update)
# TODO delete channel from wallet storage?
def watch_channel(self, chan, sweep_address, 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,
last_ctn_their_ctx=0,
last_ctn_revoked_pcs=-1)
self.funding_txo_spent_callback[chan.funding_outpoint] = callback_funding_txo_spent
self.write_to_disk()
@aiosafe
async def on_network_update(self, event, *args):
if not self.addr_sync.synchronizer:
self.print_error("synchronizer not set yet")
return
if not self.addr_sync.synchronizer.is_up_to_date():
return
with self.lock:
channel_info_items = list(self.channel_info.items())
for address, info in channel_info_items:
await self.check_onchain_situation(info.outpoint)
def watch_address(self, addr):
with self.lock:
self.watched_addresses.add(addr)
self.wallet.synchronizer.add(addr)
self.addr_sync.synchronizer.add(addr)
async def check_onchain_situation(self):
funding_outpoint = self.chan.funding_outpoint
ctx_candidate_txid = self.wallet.spent_outpoints[funding_outpoint.txid].get(funding_outpoint.output_index)
if ctx_candidate_txid is None:
async def check_onchain_situation(self, funding_outpoint):
ctx_candidate_txid = self.addr_sync.spent_outpoints[funding_outpoint.txid].get(funding_outpoint.output_index)
# call funding_txo_spent_callback if there is one
is_funding_txo_spent = ctx_candidate_txid is not None
cb = self.funding_txo_spent_callback.get(funding_outpoint)
if cb: cb(is_funding_txo_spent)
if not is_funding_txo_spent:
return
ctx_candidate = self.wallet.transactions.get(ctx_candidate_txid)
ctx_candidate = self.addr_sync.transactions.get(ctx_candidate_txid)
if ctx_candidate is None:
return
#self.print_error("funding outpoint {} is spent by {}"
# .format(funding_outpoint, ctx_candidate_txid))
for i, txin in enumerate(ctx_candidate.inputs()):
if txin['type'] == 'coinbase': continue
prevout_hash = txin['prevout_hash']
prevout_n = txin['prevout_n']
if prevout_hash == funding_outpoint.txid and prevout_n == funding_outpoint.output_index:
break
else:
raise Exception('{} is supposed to be spent by {}, but none of the inputs spend it'
.format(funding_outpoint, ctx_candidate_txid))
conf = self.wallet.get_tx_height(ctx_candidate_txid).conf
conf = self.addr_sync.get_tx_height(ctx_candidate_txid).conf
# only care about confirmed and verified ctxs. TODO is this necessary?
if conf == 0:
return
keep_watching_this = await self.inspect_ctx_candidate(ctx_candidate, i)
keep_watching_this = await self.inspect_ctx_candidate(funding_outpoint, ctx_candidate)
if not keep_watching_this:
self.stop_and_delete()
self.stop_and_delete(funding_outpoint)
# TODO batch sweeps
# TODO sweep HTLC outputs
async def inspect_ctx_candidate(self, ctx, txin_idx: int):
def stop_and_delete(self, funding_outpoint):
# TODO delete channel from watcher_db
pass
async def inspect_ctx_candidate(self, funding_outpoint, ctx):
"""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
not_yet_watching = False
for o in ctx.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()
with self.lock:
encumbered_sweep_txns = self.sweepstore[funding_outpoint.to_str()][ctx_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))
return False
# check if any response applies
keep_watching_this = False
chan = self.chan
ctn = extract_ctn_from_tx(ctx, txin_idx,
chan.local_config.payment_basepoint.pubkey,
chan.remote_config.payment_basepoint.pubkey)
latest_local_ctn = chan.local_state.ctn
latest_remote_ctn = chan.remote_state.ctn
self.print_error("ctx {} has ctn {}. latest local ctn is {}, latest remote ctn is {}"
.format(ctx.txid(), ctn, latest_local_ctn, latest_remote_ctn))
# see if it is a normal unilateral close by them
if ctn == latest_remote_ctn:
# note that we might also get here if this is our ctx and the ctn just happens to match
their_cur_pcp = chan.remote_state.current_per_commitment_point
if their_cur_pcp is not None:
keep_watching_this |= await self.find_and_sweep_their_ctx_to_remote(ctx, their_cur_pcp)
# see if we have a revoked secret for this ctn ("breach")
local_height = self.network.get_local_height()
for e_tx in 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
num_conf = local_height - tx_height + 1
if num_conf >= e_tx.csv_delay:
await self.network.broadcast_transaction(e_tx.tx, self.print_tx_broadcast_result)
else:
self.print_error('waiting for CSV ({} < {}) for funding outpoint {} and ctx {}'
.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:
per_commitment_secret = chan.remote_state.revocation_store.retrieve_secret(
RevocationStore.START_INDEX - ctn)
except UnableToDeriveSecret:
self.print_error("revocation store does not have secret for ctx {}".format(ctx.txid()))
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]
except KeyError:
return -1
if ours:
return ci.last_ctn_our_ctx
else:
# note that we might also get here if this is our ctx and we just happen to have
# the secret for the symmetric ctn
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
keep_watching_this |= await self.find_and_sweep_their_ctx_to_remote(ctx, their_pcp)
keep_watching_this |= await self.find_and_sweep_their_ctx_to_local(ctx, per_commitment_secret)
# see if it's our ctx
our_per_commitment_secret = get_per_commitment_secret_from_seed(
chan.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_per_commitment_point = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
keep_watching_this |= await self.find_and_sweep_our_ctx_to_local(ctx, our_per_commitment_point)
return keep_watching_this
return ci.last_ctn_their_ctx
def _inc_last_ctn_for_processed_ctx(self, funding_address: str, ours: bool) -> None:
try:
ci = self.channel_info[funding_address]
except KeyError:
return
if ours:
ci = ci._replace(last_ctn_our_ctx=ci.last_ctn_our_ctx + 1)
else:
ci = ci._replace(last_ctn_their_ctx=ci.last_ctn_their_ctx + 1)
self.channel_info[funding_address] = ci
def get_tx_mined_status(self, txid):
def _get_last_ctn_for_revoked_secret(self, funding_address: str) -> int:
try:
ci = self.channel_info[funding_address]
except KeyError:
return -1
return ci.last_ctn_revoked_pcs
def _inc_last_ctn_for_revoked_secret(self, funding_address: str) -> None:
try:
ci = self.channel_info[funding_address]
except KeyError:
return
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
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._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
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._inc_last_ctn_for_revoked_secret(funding_address)
self.write_to_disk()
def add_to_sweepstore(self, funding_outpoint: str, ctx_txid: str, encumbered_sweeptx: EncumberedTransaction):
if encumbered_sweeptx is None:
return
with self.lock:
self.sweepstore[funding_outpoint][ctx_txid].add(encumbered_sweeptx)
def get_tx_mined_status(self, txid: str):
if not txid:
return TX_MINED_STATUS_FREE
tx_mined_status = self.wallet.get_tx_height(txid)
tx_mined_status = self.addr_sync.get_tx_height(txid)
height, conf = tx_mined_status.height, tx_mined_status.conf
if conf > 100:
return TX_MINED_STATUS_DEEP
@ -161,115 +320,12 @@ class LNChanCloseHandler(PrintError):
else:
raise NotImplementedError()
async def find_and_sweep_their_ctx_to_remote(self, ctx, their_pcp: bytes):
"""Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point."""
payment_bp_privkey = ecc.ECPrivkey(self.chan.local_config.payment_basepoint.privkey)
our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
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, o in enumerate(ctx.outputs()):
if o.type == TYPE_ADDRESS and o.address == to_remote_address:
self.print_error("found to_remote output paying to us: ctx {}:{}".
format(ctx.txid(), output_idx))
#self.print_error("ctx {} is normal unilateral close by them".format(ctx.txid()))
break
else:
return False
if to_remote_address not in self.watched_addresses:
self.watch_address(to_remote_address)
return True
spending_txid = self.wallet.spent_outpoints[ctx.txid()].get(output_idx)
stx_mined_status = self.get_tx_mined_status(spending_txid)
if stx_mined_status == TX_MINED_STATUS_DEEP:
return False
elif stx_mined_status in (TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL):
return True
sweep_tx = create_sweeptx_their_ctx_to_remote(self.network, self.sweep_address, ctx,
output_idx, our_payment_privkey)
res = await self.network.broadcast_transaction(sweep_tx)
self.print_tx_broadcast_result('sweep_their_ctx_to_remote', res)
return True
def get_deepest_tx_mined_status_for_txids(self, set_of_txids: Iterable[str]):
if not set_of_txids:
return TX_MINED_STATUS_FREE
# note: using "min" as lower status values are deeper
return min(map(self.get_tx_mined_status, set_of_txids))
async def find_and_sweep_their_ctx_to_local(self, ctx, per_commitment_secret: bytes):
"""Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point."""
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
revocation_privkey = lnutil.derive_blinded_privkey(self.chan.local_config.revocation_basepoint.privkey,
per_commitment_secret)
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
to_self_delay = self.chan.local_config.to_self_delay
delayed_pubkey = derive_pubkey(self.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:
self.print_error("found to_local output paying to them: ctx {}:{}".
format(ctx.txid(), output_idx))
break
else:
self.print_error('could not find to_local output in their ctx {}'.format(ctx.txid()))
return False
if to_local_address not in self.watched_addresses:
self.watch_address(to_local_address)
return True
spending_txid = self.wallet.spent_outpoints[ctx.txid()].get(output_idx)
stx_mined_status = self.get_tx_mined_status(spending_txid)
if stx_mined_status == TX_MINED_STATUS_DEEP:
return False
elif stx_mined_status in (TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL):
return True
sweep_tx = create_sweeptx_ctx_to_local(self.network, self.sweep_address, ctx, output_idx,
witness_script, revocation_privkey, True)
res = await self.network.broadcast_transaction(sweep_tx)
self.print_tx_broadcast_result('sweep_their_ctx_to_local', res)
return True
async def find_and_sweep_our_ctx_to_local(self, ctx, our_pcp: bytes):
"""Returns True iff found a not-deeply-spent output that we could
potentially sweep at some point."""
delayed_bp_privkey = ecc.ECPrivkey(self.chan.local_config.delayed_basepoint.privkey)
our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
revocation_pubkey = lnutil.derive_blinded_pubkey(self.chan.remote_config.revocation_basepoint.pubkey,
our_pcp)
to_self_delay = self.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:
self.print_error("found to_local output paying to us (CSV-locked): ctx {}:{}".
format(ctx.txid(), output_idx))
break
else:
self.print_error('could not find to_local output in our ctx {}'.format(ctx.txid()))
return False
if to_local_address not in self.watched_addresses:
self.watch_address(to_local_address)
return True
spending_txid = self.wallet.spent_outpoints[ctx.txid()].get(output_idx)
stx_mined_status = self.get_tx_mined_status(spending_txid)
if stx_mined_status == TX_MINED_STATUS_DEEP:
return False
elif stx_mined_status in (TX_MINED_STATUS_SHALLOW, TX_MINED_STATUS_MEMPOOL):
return True
# check timelock
ctx_num_conf = self.wallet.get_tx_height(ctx.txid()).conf
if to_self_delay > ctx_num_conf:
self.print_error('waiting for CSV ({} < {}) for ctx {}'.format(ctx_num_conf, to_self_delay, ctx.txid()))
return True
sweep_tx = create_sweeptx_ctx_to_local(self.network, self.sweep_address, ctx, output_idx,
witness_script, our_localdelayed_privkey.get_secret_bytes(),
False, to_self_delay)
res = await self.network.broadcast_transaction(sweep_tx)
self.print_tx_broadcast_result('sweep_our_ctx_to_local', res)
return True
def print_tx_broadcast_result(self, name, res):
error, msg = res
@ -279,9 +335,87 @@ class LNChanCloseHandler(PrintError):
self.print_error('{} broadcast succeeded'.format(name))
def create_sweeptx_their_ctx_to_remote(network, address, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey):
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][2]
val = ctx.outputs()[output_idx].value
sweep_inputs = [{
'type': 'p2wpkh',
'x_pubkeys': [our_payment_pubkey],
@ -293,14 +427,10 @@ def create_sweeptx_their_ctx_to_remote(network, address, ctx, output_idx: int, o
'signatures': [None],
}]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
try:
fee = network.config.estimate_fee(tx_size_bytes)
except NoDynamicFeeEstimates:
fee_per_kb = network.config.fee_per_kb(dyn=False)
fee = network.config.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
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)]
locktime = network.get_local_height()
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, locktime=locktime)
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():
@ -308,15 +438,17 @@ def create_sweeptx_their_ctx_to_remote(network, address, ctx, output_idx: int, o
return sweep_tx
def create_sweeptx_ctx_to_local(network, address, ctx, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool, to_self_delay: int=None):
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][2]
val = ctx.outputs()[output_idx].value
sweep_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
@ -331,14 +463,10 @@ def create_sweeptx_ctx_to_local(network, address, ctx, output_idx: int, witness_
if to_self_delay is not None:
sweep_inputs[0]['sequence'] = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
try:
fee = network.config.estimate_fee(tx_size_bytes)
except NoDynamicFeeEstimates:
fee_per_kb = network.config.fee_per_kb(dyn=False)
fee = network.config.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
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)]
locktime = network.get_local_height()
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, locktime=locktime, version=2)
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

31
electrum/lnworker.py

@ -5,20 +5,20 @@ import random
import time
from typing import Optional, Sequence
import threading
from functools import partial
import dns.resolver
import dns.exception
from . import constants
from .bitcoin import sha256, COIN
from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, aiosafe
from .lnbase import Peer, privkey_to_pubkey
from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv
from .lnbase import Peer, privkey_to_pubkey, aiosafe
from .lnaddr import lnencode, LnAddr, lndecode
from .ecc import der_sig_from_sig_string
from .lnhtlc import HTLCStateMachine
from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32,
PaymentFailure)
from .lnwatcher import LNChanCloseHandler
from .i18n import _
@ -35,6 +35,7 @@ class LNWorker(PrintError):
def __init__(self, wallet, network):
self.wallet = wallet
self.sweep_address = wallet.get_receiving_address()
self.network = network
self.channel_db = self.network.channel_db
self.lock = threading.RLock()
@ -48,9 +49,11 @@ class LNWorker(PrintError):
self.config = network.config
self.peers = {} # pubkey -> Peer
self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))}
for c in self.channels.values():
c.lnwatcher = network.lnwatcher
self.invoices = wallet.storage.get('lightning_invoices', {})
for chan_id, chan in self.channels.items():
self.network.lnwatcher.watch_channel(chan, self.on_channel_utxos)
self.network.lnwatcher.watch_channel(chan, self.sweep_address, partial(self.on_channel_utxos, chan))
self._last_tried_peer = {} # LNPeerAddr -> unix timestamp
self._add_peers_from_config()
# wait until we see confirmations
@ -118,16 +121,11 @@ class LNWorker(PrintError):
return True
return False
async def on_channel_utxos(self, chan, utxos):
outpoints = [Outpoint(x["tx_hash"], x["tx_pos"]) for x in utxos]
if chan.funding_outpoint not in outpoints:
chan.set_funding_txo_spentness(True)
def on_channel_utxos(self, chan, is_funding_txo_spent: bool):
chan.set_funding_txo_spentness(is_funding_txo_spent)
if is_funding_txo_spent:
chan.set_state("CLOSED")
self.channel_db.remove_channel(chan.short_channel_id)
# FIXME is this properly GC-ed? (or too soon?)
LNChanCloseHandler(self.network, self.wallet, chan)
else:
chan.set_funding_txo_spentness(False)
self.network.trigger_callback('channel', chan)
@aiosafe
@ -138,7 +136,6 @@ class LNWorker(PrintError):
with self.lock:
channels = list(self.channels.values())
for chan in channels:
print("update", chan.get_state())
if chan.get_state() == "OPENING":
res = self.save_short_chan_id(chan)
if not res:
@ -159,12 +156,16 @@ class LNWorker(PrintError):
async def _open_channel_coroutine(self, node_id, local_amount_sat, push_sat, password):
peer = self.peers[node_id]
openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, local_amount_sat + push_sat, push_sat * 1000, temp_channel_id=os.urandom(32))
openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password,
funding_sat=local_amount_sat + push_sat,
push_msat=push_sat * 1000,
temp_channel_id=os.urandom(32),
sweep_address=self.sweep_address)
if not openingchannel:
self.print_error("Channel_establishment_flow returned None")
return
self.save_channel(openingchannel)
self.network.lnwatcher.watch_channel(openingchannel, self.on_channel_utxos)
self.network.lnwatcher.watch_channel(openingchannel, self.sweep_address, partial(self.on_channel_utxos, openingchannel))
self.on_channels_updated()
def on_channels_updated(self):

3
electrum/tests/test_lnhtlc.py

@ -64,7 +64,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
feerate=local_feerate
),
"constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3),
"node_id":other_node_id
"node_id":other_node_id,
"remote_commitment_to_be_revoked": None,
}
def bip32(sequence):

Loading…
Cancel
Save