lnwatcher: in inspect_tx_candidate, match witness scripts against HTLC templates
fixes #7781
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user