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 .i18n import _
|
||||||
from .lnaddr import lndecode
|
from .lnaddr import lndecode
|
||||||
from .bip32 import BIP32Node, BIP32_PRIME
|
from .bip32 import BIP32Node, BIP32_PRIME
|
||||||
from .transaction import BCDataStream
|
from .transaction import BCDataStream, OPPushDataGeneric
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .lnchannel import Channel, AbstractChannel
|
from .lnchannel import Channel, AbstractChannel
|
||||||
@@ -636,6 +637,68 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
|
|||||||
]))
|
]))
|
||||||
return script
|
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,
|
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:
|
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes:
|
||||||
if is_received_htlc:
|
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 .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 .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED
|
||||||
from .transaction import Transaction, TxOutpoint
|
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:
|
if TYPE_CHECKING:
|
||||||
from .network import Network
|
from .network import Network
|
||||||
@@ -222,24 +225,46 @@ class LNWatcher(AddressSynchronizer):
|
|||||||
raise NotImplementedError() # implemented by subclasses
|
raise NotImplementedError() # implemented by subclasses
|
||||||
|
|
||||||
def inspect_tx_candidate(self, outpoint, n):
|
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(':')
|
prev_txid, index = outpoint.split(':')
|
||||||
txid = self.db.get_spent_outpoint(prev_txid, int(index))
|
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
|
||||||
result = {outpoint:txid}
|
result = {outpoint:spender_txid}
|
||||||
if txid is None:
|
if n == 0:
|
||||||
self.channel_status[outpoint] = 'open'
|
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
|
return result
|
||||||
if n == 0 and not self.is_deeply_mined(txid):
|
spender_tx = self.db.get_transaction(spender_txid)
|
||||||
self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(txid).conf
|
if n == 1:
|
||||||
else:
|
# if tx input is not a first-stage HTLC, we can stop recursion
|
||||||
self.channel_status[outpoint] = 'closed (deep)'
|
if len(spender_tx.inputs()) != 1:
|
||||||
tx = self.db.get_transaction(txid)
|
return result
|
||||||
for i, o in enumerate(tx.outputs()):
|
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:
|
if o.address is None:
|
||||||
continue
|
continue
|
||||||
if not self.is_mine(o.address):
|
if not self.is_mine(o.address):
|
||||||
self.add_address(o.address)
|
self.add_address(o.address)
|
||||||
elif n < 2:
|
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)
|
result.update(r)
|
||||||
return result
|
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})')
|
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'."""
|
"""Returns whether 'script' matches 'template'."""
|
||||||
if script is None:
|
if script is None:
|
||||||
return False
|
return False
|
||||||
@@ -491,8 +491,14 @@ def match_script_against_template(script, template) -> bool:
|
|||||||
try:
|
try:
|
||||||
script = [x for x in script_GetOp(script)]
|
script = [x for x in script_GetOp(script)]
|
||||||
except MalformedBitcoinScript:
|
except MalformedBitcoinScript:
|
||||||
|
if debug:
|
||||||
|
_logger.debug(f"malformed script")
|
||||||
return False
|
return False
|
||||||
|
if debug:
|
||||||
|
_logger.debug(f"match script against template: {script}")
|
||||||
if len(script) != len(template):
|
if len(script) != len(template):
|
||||||
|
if debug:
|
||||||
|
_logger.debug(f"length mismatch {len(script)} != {len(template)}")
|
||||||
return False
|
return False
|
||||||
for i in range(len(script)):
|
for i in range(len(script)):
|
||||||
template_item = template[i]
|
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]):
|
if OPGeneric.is_instance(template_item) and template_item.match(script_item[0]):
|
||||||
continue
|
continue
|
||||||
if template_item != script_item[0]:
|
if template_item != script_item[0]:
|
||||||
|
if debug:
|
||||||
|
_logger.debug(f"item mismatch at position {i}: {template_item} != {script_item[0]}")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user