Skip to content

Commit

Permalink
check dust limits
Browse files Browse the repository at this point in the history
* on channel opening we verify that the peer's dust limit is above 354
  sat, the limit for unknown segwit versions
* we constrain the allowed scriptpubkey types for channel closing
* we check that the remote's output is above the relay dust limit for
  the collaborative close case
  • Loading branch information
bitromortac committed Oct 22, 2021
1 parent 1a75aa7 commit b339279
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 15 deletions.
8 changes: 6 additions & 2 deletions electrum/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,12 @@ def relayfee(network: 'Network' = None) -> int:


# see https://github.com/bitcoin/bitcoin/blob/a62f0ed64f8bbbdfe6467ac5ce92ef5b5222d1bd/src/policy/policy.cpp#L14
DUST_LIMIT_DEFAULT_SAT_LEGACY = 546
DUST_LIMIT_DEFAULT_SAT_SEGWIT = 294
# and https://github.com/lightningnetwork/lightning-rfc/blob/7e3dce42cbe4fa4592320db6a4e06c26bb99122b/03-transactions.md#dust-limits
DUST_LIMIT_P2PKH = 546
DUST_LIMIT_P2SH = 540
DUST_LIMIT_UNKNOWN_SEGWIT = 354
DUST_LIMIT_P2WSH = 330
DUST_LIMIT_P2WPKH = 294


def dust_threshold(network: 'Network' = None) -> int:
Expand Down
30 changes: 20 additions & 10 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn
static_remotekey = bfh(wallet.get_public_key(addr))
else:
static_remotekey = None
dust_limit_sat = bitcoin.DUST_LIMIT_DEFAULT_SAT_LEGACY
# TODO: should we keep it this high, or go lower?
dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH
reserve_sat = max(funding_sat // 100, dust_limit_sat)
# for comparison of defaults, see
# https://github.com/ACINQ/eclair/blob/afa378fbb73c265da44856b4ad0f2128a88ae6c6/eclair-core/src/main/resources/reference.conf#L66
Expand Down Expand Up @@ -1698,11 +1699,7 @@ async def on_shutdown(self, chan: Channel, payload):
else:
# BOLT-02 restrict the scriptpubkey to some templates:
# order by decreasing dust limit
if match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2PKH):
pass
if match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2SH):
pass
elif self.is_shutdown_anysegwit() and match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT):
if self.is_shutdown_anysegwit() and match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT):
pass
if match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):
pass
Expand Down Expand Up @@ -1765,9 +1762,9 @@ async def _shutdown(self, chan: Channel, payload, *, is_local: bool):
# BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx
max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)
our_fee = min(our_fee, max_fee)
drop_remote = False
drop_to_remote = False
def send_closing_signed():
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_remote)
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_to_remote)
self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig)
def verify_signature(tx, sig):
their_pubkey = chan.config[REMOTE].multisig_key.pubkey
Expand All @@ -1788,13 +1785,26 @@ def verify_signature(tx, sig):
# verify their sig: they might have dropped their output
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=False)
if verify_signature(closing_tx, their_sig):
drop_remote = False
drop_to_remote = False
else:
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=True)
if verify_signature(closing_tx, their_sig):
drop_remote = True
drop_to_remote = True
else:
raise Exception('failed to verify their signature')
# at this point we know how their closing tx looks like
# check that the remote's output is above our dust limit
to_remote_idx = closing_tx.get_output_idxs_from_scriptpubkey(their_scriptpubkey).pop()
to_remote_amount = closing_tx.outputs()[to_remote_idx].value
if not drop_to_remote:
dust_limit = chan.config[LOCAL].dust_limit_sat
if to_remote_amount <= dust_limit:
# should we force-close here?
raise Exception(f'to_remote output ({to_remote_amount} sat) is too small for our dust limit ({dust_limit} sat)')
# check that their output is above their scriptpubkey's dust limit
if not drop_to_remote:
transaction.check_scriptpubkey_template_and_dust(their_scriptpubkey, to_remote_amount)

# Agree if difference is lower or equal to one (see below)
if abs(our_fee - their_fee) < 2:
our_fee = their_fee
Expand Down
7 changes: 4 additions & 3 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
HTLC_OUTPUT_WEIGHT = 172

LN_MAX_FUNDING_SAT = pow(2, 24) - 1
DUST_LIMIT_MAX = 3000 # TODO: find a better number

# dummy address for fee estimation of funding tx
def ln_dummy_address():
Expand Down Expand Up @@ -103,10 +104,10 @@ def validate_params(self, *, funding_sat: int) -> None:
raise Exception(f"{conf_name}. insane initial_msat={self.initial_msat}. (funding_sat={funding_sat})")
if self.reserve_sat < self.dust_limit_sat:
raise Exception(f"{conf_name}. MUST set channel_reserve_satoshis greater than or equal to dust_limit_satoshis")
# technically this could be using the lower DUST_LIMIT_DEFAULT_SAT_SEGWIT
# but other implementations are checking against this value too; also let's be conservative
if self.dust_limit_sat < bitcoin.DUST_LIMIT_DEFAULT_SAT_LEGACY:
if self.dust_limit_sat < bitcoin.DUST_LIMIT_UNKNOWN_SEGWIT:
raise Exception(f"{conf_name}. dust limit too low: {self.dust_limit_sat} sat")
if self.dust_limit_sat < DUST_LIMIT_MAX:
raise Exception(f"{conf_name}. dust limit too high: {self.dust_limit_sat} sat")
if self.reserve_sat > funding_sat // 100:
raise Exception(f"{conf_name}. reserve too high: {self.reserve_sat}, funding_sat: {funding_sat}")
if self.htlc_minimum_msat > 1_000:
Expand Down
15 changes: 15 additions & 0 deletions electrum/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,21 @@ def is_instance(cls, item):
SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT = [OP_ANYSEGWIT_VERSION, OPPushDataAnySegwit]


def check_scriptpubkey_template_and_dust(scriptpubkey, amount: Optional[int]):
if match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2PKH):
dust_limit = bitcoin.DUST_LIMIT_P2PKH
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2SH):
dust_limit = bitcoin.DUST_LIMIT_P2SH
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WSH):
dust_limit = bitcoin.DUST_LIMIT_P2WSH
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WPKH):
dust_limit = bitcoin.DUST_LIMIT_P2WPKH
else:
raise Exception(f'scriptpubkey does not conform to any template: {scriptpubkey.hex()}')
if amount < dust_limit:
raise Exception(f'amount ({amount}) is below dust limit for scriptpubkey type ({dust_limit})')


def match_script_against_template(script, template) -> bool:
"""Returns whether 'script' matches 'template'."""
if script is None:
Expand Down

0 comments on commit b339279

Please sign in to comment.