Browse Source

lnwatcher: in inspect_tx_candidate, match witness scripts against HTLC templates

fixes #7781
patch-4
ThomasV 3 years ago
parent
commit
44f29331bf
  1. 65
      electrum/lnutil.py
  2. 47
      electrum/lnwatcher.py
  3. 10
      electrum/transaction.py

65
electrum/lnutil.py

@ -24,7 +24,8 @@ from . import segwit_addr
from .i18n import _
from .lnaddr import lndecode
from .bip32 import BIP32Node, BIP32_PRIME
from .transaction import BCDataStream
from .transaction import BCDataStream, OPPushDataGeneric
if TYPE_CHECKING:
from .lnchannel import Channel, AbstractChannel
@ -636,6 +637,68 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
]))
return script
WITNESS_TEMPLATE_OFFERED_HTLC = [
opcodes.OP_DUP,
opcodes.OP_HASH160,
OPPushDataGeneric(None),
opcodes.OP_EQUAL,
opcodes.OP_IF,
opcodes.OP_CHECKSIG,
opcodes.OP_ELSE,
OPPushDataGeneric(None),
opcodes.OP_SWAP,
opcodes.OP_SIZE,
OPPushDataGeneric(lambda x: x==1),
opcodes.OP_EQUAL,
opcodes.OP_NOTIF,
opcodes.OP_DROP,
opcodes.OP_2,
opcodes.OP_SWAP,
OPPushDataGeneric(None),
opcodes.OP_2,
opcodes.OP_CHECKMULTISIG,
opcodes.OP_ELSE,
opcodes.OP_HASH160,
OPPushDataGeneric(None),
opcodes.OP_EQUALVERIFY,
opcodes.OP_CHECKSIG,
opcodes.OP_ENDIF,
opcodes.OP_ENDIF,
]
WITNESS_TEMPLATE_RECEIVED_HTLC = [
opcodes.OP_DUP,
opcodes.OP_HASH160,
OPPushDataGeneric(None),
opcodes.OP_EQUAL,
opcodes.OP_IF,
opcodes.OP_CHECKSIG,
opcodes.OP_ELSE,
OPPushDataGeneric(None),
opcodes.OP_SWAP,
opcodes.OP_SIZE,
OPPushDataGeneric(lambda x: x==1),
opcodes.OP_EQUAL,
opcodes.OP_IF,
opcodes.OP_HASH160,
OPPushDataGeneric(None),
opcodes.OP_EQUALVERIFY,
opcodes.OP_2,
opcodes.OP_SWAP,
OPPushDataGeneric(None),
opcodes.OP_2,
opcodes.OP_CHECKMULTISIG,
opcodes.OP_ELSE,
opcodes.OP_DROP,
OPPushDataGeneric(None),
opcodes.OP_CHECKLOCKTIMEVERIFY,
opcodes.OP_DROP,
opcodes.OP_CHECKSIG,
opcodes.OP_ENDIF,
opcodes.OP_ENDIF,
]
def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes,
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes:
if is_received_htlc:

47
electrum/lnwatcher.py

@ -14,6 +14,9 @@ from .wallet_db import WalletDB
from .util import bh2u, bfh, log_exceptions, ignore_exceptions, TxMinedInfo, random_shuffled_copy
from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED
from .transaction import Transaction, TxOutpoint
from .transaction import match_script_against_template
from .lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC
if TYPE_CHECKING:
from .network import Network
@ -222,24 +225,46 @@ class LNWatcher(AddressSynchronizer):
raise NotImplementedError() # implemented by subclasses
def inspect_tx_candidate(self, outpoint, n):
"""
returns a dict of spenders for a transaction of interest.
subscribes to addresses as a side effect.
n==0 => outpoint is a channel funding.
n==1 => outpoint is a commitment or close output: to_local, to_remote or first-stage htlc
n==2 => outpoint is a second-stage htlc
"""
prev_txid, index = outpoint.split(':')
txid = self.db.get_spent_outpoint(prev_txid, int(index))
result = {outpoint:txid}
if txid is None:
self.channel_status[outpoint] = 'open'
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
result = {outpoint:spender_txid}
if n == 0:
if spender_txid is None:
self.channel_status[outpoint] = 'open'
elif not self.is_deeply_mined(spender_txid):
self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(spender_txid).conf
else:
self.channel_status[outpoint] = 'closed (deep)'
if spender_txid is None:
return result
if n == 0 and not self.is_deeply_mined(txid):
self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(txid).conf
else:
self.channel_status[outpoint] = 'closed (deep)'
tx = self.db.get_transaction(txid)
for i, o in enumerate(tx.outputs()):
spender_tx = self.db.get_transaction(spender_txid)
if n == 1:
# if tx input is not a first-stage HTLC, we can stop recursion
if len(spender_tx.inputs()) != 1:
return result
o = spender_tx.inputs()[0]
witness = o.witness_elements()
redeem_script = witness[-1]
if match_script_against_template(redeem_script, WITNESS_TEMPLATE_OFFERED_HTLC):
self.logger.info(f"input script matches offered htlc {redeem_script.hex()}")
elif match_script_against_template(redeem_script, WITNESS_TEMPLATE_RECEIVED_HTLC):
self.logger.info(f"input script matches received htlc {redeem_script.hex()}")
else:
return result
for i, o in enumerate(spender_tx.outputs()):
if o.address is None:
continue
if not self.is_mine(o.address):
self.add_address(o.address)
elif n < 2:
r = self.inspect_tx_candidate(txid+':%d'%i, n+1)
r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1)
result.update(r)
return result

10
electrum/transaction.py

@ -482,7 +482,7 @@ def check_scriptpubkey_template_and_dust(scriptpubkey, amount: Optional[int]):
raise Exception(f'amount ({amount}) is below dust limit for scriptpubkey type ({dust_limit})')
def match_script_against_template(script, template) -> bool:
def match_script_against_template(script, template, debug=False) -> bool:
"""Returns whether 'script' matches 'template'."""
if script is None:
return False
@ -491,8 +491,14 @@ def match_script_against_template(script, template) -> bool:
try:
script = [x for x in script_GetOp(script)]
except MalformedBitcoinScript:
if debug:
_logger.debug(f"malformed script")
return False
if debug:
_logger.debug(f"match script against template: {script}")
if len(script) != len(template):
if debug:
_logger.debug(f"length mismatch {len(script)} != {len(template)}")
return False
for i in range(len(script)):
template_item = template[i]
@ -502,6 +508,8 @@ def match_script_against_template(script, template) -> bool:
if OPGeneric.is_instance(template_item) and template_item.match(script_item[0]):
continue
if template_item != script_item[0]:
if debug:
_logger.debug(f"item mismatch at position {i}: {template_item} != {script_item[0]}")
return False
return True

Loading…
Cancel
Save