Skip to content

Commit 5787f3a

Browse files
authored
Merge pull request #7542 from bitromortac/2109-dust-limit
Implement recent spec changes regarding collab channel close outputs
2 parents cb55a2d + e97f350 commit 5787f3a

File tree

6 files changed

+91
-18
lines changed

6 files changed

+91
-18
lines changed

electrum/bitcoin.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,12 @@ def relayfee(network: 'Network' = None) -> int:
349349

350350

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

355359

356360
def dust_threshold(network: 'Network' = None) -> int:

electrum/lnpeer.py

+28-10
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ def close_and_cleanup(self):
495495
self.lnworker.peer_closed(self)
496496
self.got_disconnected.set()
497497

498+
def is_shutdown_anysegwit(self):
499+
return self.features.supports(LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT)
500+
498501
def is_static_remotekey(self):
499502
return self.features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
500503

@@ -531,7 +534,7 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn
531534
static_remotekey = bfh(wallet.get_public_key(addr))
532535
else:
533536
static_remotekey = None
534-
dust_limit_sat = bitcoin.DUST_LIMIT_DEFAULT_SAT_LEGACY
537+
dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH
535538
reserve_sat = max(funding_sat // 100, dust_limit_sat)
536539
# for comparison of defaults, see
537540
# https://github.com/ACINQ/eclair/blob/afa378fbb73c265da44856b4ad0f2128a88ae6c6/eclair-core/src/main/resources/reference.conf#L66
@@ -1343,6 +1346,8 @@ def maybe_forward_htlc(
13431346
# - for example; atm we forward first and then persist "forwarding_info",
13441347
# so if we segfault in-between and restart, we might forward an HTLC twice...
13451348
# (same for trampoline forwarding)
1349+
# - we could check for the exposure to dust HTLCs, see:
1350+
# https://github.com/ACINQ/eclair/pull/1985
13461351
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
13471352
if not forwarding_enabled:
13481353
self.logger.info(f"forwarding is disabled. failing htlc.")
@@ -1692,11 +1697,15 @@ async def on_shutdown(self, chan: Channel, payload):
16921697
if their_upfront_scriptpubkey:
16931698
if not (their_scriptpubkey == their_upfront_scriptpubkey):
16941699
raise UpfrontShutdownScriptViolation("remote didn't use upfront shutdown script it commited to in channel opening")
1695-
# BOLT-02 restrict the scriptpubkey to some templates:
1696-
if not (match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0)
1697-
or match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2SH)
1698-
or match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2PKH)):
1699-
raise Exception(f'scriptpubkey in received shutdown message does not conform to any template: {their_scriptpubkey.hex()}')
1700+
else:
1701+
# BOLT-02 restrict the scriptpubkey to some templates:
1702+
if self.is_shutdown_anysegwit() and match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT):
1703+
pass
1704+
elif match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):
1705+
pass
1706+
else:
1707+
raise Exception(f'scriptpubkey in received shutdown message does not conform to any template: {their_scriptpubkey.hex()}')
1708+
17001709
chan_id = chan.channel_id
17011710
if chan_id in self.shutdown_received:
17021711
self.shutdown_received[chan_id].set_result(payload)
@@ -1753,9 +1762,9 @@ async def _shutdown(self, chan: Channel, payload, *, is_local: bool):
17531762
# BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx
17541763
max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)
17551764
our_fee = min(our_fee, max_fee)
1756-
drop_remote = False
1765+
drop_to_remote = False
17571766
def send_closing_signed():
1758-
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_remote)
1767+
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_to_remote)
17591768
self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig)
17601769
def verify_signature(tx, sig):
17611770
their_pubkey = chan.config[REMOTE].multisig_key.pubkey
@@ -1776,13 +1785,22 @@ def verify_signature(tx, sig):
17761785
# verify their sig: they might have dropped their output
17771786
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=False)
17781787
if verify_signature(closing_tx, their_sig):
1779-
drop_remote = False
1788+
drop_to_remote = False
17801789
else:
17811790
our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=True)
17821791
if verify_signature(closing_tx, their_sig):
1783-
drop_remote = True
1792+
drop_to_remote = True
17841793
else:
1794+
# this can happen if we consider our output too valuable to drop,
1795+
# but the remote drops it because it violates their dust limit
17851796
raise Exception('failed to verify their signature')
1797+
# at this point we know how the closing tx looks like
1798+
# check that their output is above their scriptpubkey's network dust limit
1799+
if not drop_to_remote:
1800+
to_remote_idx = closing_tx.get_output_idxs_from_scriptpubkey(their_scriptpubkey.hex()).pop()
1801+
to_remote_amount = closing_tx.outputs()[to_remote_idx].value
1802+
transaction.check_scriptpubkey_template_and_dust(their_scriptpubkey, to_remote_amount)
1803+
17861804
# Agree if difference is lower or equal to one (see below)
17871805
if abs(our_fee - their_fee) < 2:
17881806
our_fee = their_fee

electrum/lnutil.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
HTLC_OUTPUT_WEIGHT = 172
4141

4242
LN_MAX_FUNDING_SAT = pow(2, 24) - 1
43+
DUST_LIMIT_MAX = 1000
4344

4445
# dummy address for fee estimation of funding tx
4546
def ln_dummy_address():
@@ -103,10 +104,10 @@ def validate_params(self, *, funding_sat: int) -> None:
103104
raise Exception(f"{conf_name}. insane initial_msat={self.initial_msat}. (funding_sat={funding_sat})")
104105
if self.reserve_sat < self.dust_limit_sat:
105106
raise Exception(f"{conf_name}. MUST set channel_reserve_satoshis greater than or equal to dust_limit_satoshis")
106-
# technically this could be using the lower DUST_LIMIT_DEFAULT_SAT_SEGWIT
107-
# but other implementations are checking against this value too; also let's be conservative
108-
if self.dust_limit_sat < bitcoin.DUST_LIMIT_DEFAULT_SAT_LEGACY:
107+
if self.dust_limit_sat < bitcoin.DUST_LIMIT_UNKNOWN_SEGWIT:
109108
raise Exception(f"{conf_name}. dust limit too low: {self.dust_limit_sat} sat")
109+
if self.dust_limit_sat > DUST_LIMIT_MAX:
110+
raise Exception(f"{conf_name}. dust limit too high: {self.dust_limit_sat} sat")
110111
if self.reserve_sat > funding_sat // 100:
111112
raise Exception(f"{conf_name}. reserve too high: {self.reserve_sat}, funding_sat: {funding_sat}")
112113
if self.htlc_minimum_msat > 1_000:
@@ -1026,6 +1027,12 @@ class LnFeatures(IntFlag):
10261027
_ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
10271028
_ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
10281029

1030+
OPTION_SHUTDOWN_ANYSEGWIT_REQ = 1 << 26
1031+
OPTION_SHUTDOWN_ANYSEGWIT_OPT = 1 << 27
1032+
1033+
_ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
1034+
_ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
1035+
10291036
# temporary
10301037
OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 50
10311038
OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 51
@@ -1114,6 +1121,7 @@ def supports(self, feature: 'LnFeatures') -> bool:
11141121
| LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ
11151122
| LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ
11161123
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ
1124+
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ
11171125
)
11181126

11191127

electrum/lnworker.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ class ErrorAddingPeer(Exception): pass
175175
| LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\
176176
| LnFeatures.GOSSIP_QUERIES_REQ\
177177
| LnFeatures.BASIC_MPP_OPT\
178-
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT
178+
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT\
179+
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\
179180

180181
LNGOSSIP_FEATURES = BASE_FEATURES\
181182
| LnFeatures.GOSSIP_QUERIES_OPT\

electrum/tests/test_transaction.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from electrum import transaction, bitcoin
44
from electrum.transaction import (convert_raw_tx_to_hex, tx_from_any, Transaction,
55
PartialTransaction, TxOutpoint, PartialTxInput,
6-
PartialTxOutput, Sighash)
6+
PartialTxOutput, Sighash, match_script_against_template,
7+
SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT)
78
from electrum.util import bh2u, bfh
89
from electrum.bitcoin import (deserialize_privkey, opcodes,
910
construct_script, construct_witness)
@@ -78,6 +79,13 @@ def test_bool(self):
7879

7980

8081
class TestTransaction(ElectrumTestCase):
82+
def test_match_against_script_template(self):
83+
script = bfh(construct_script([opcodes.OP_5, bytes(29)]))
84+
self.assertTrue(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))
85+
script = bfh(construct_script([opcodes.OP_NOP, bytes(30)]))
86+
self.assertFalse(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))
87+
script = bfh(construct_script([opcodes.OP_0, bytes(50)]))
88+
self.assertFalse(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))
8189

8290
def test_tx_update_signatures(self):
8391
tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA")
@@ -927,7 +935,7 @@ class TestSighashTypes(ElectrumTestCase):
927935
txin = PartialTxInput(prevout=prevout)
928936
txin.nsequence=0xffffffff
929937
txin.script_type='p2sh-p2wsh'
930-
txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae')
938+
txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae')
931939
txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54')
932940
txin._trusted_value_sats = 987654321
933941
#Output of Transaction

electrum/transaction.py

+34
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,23 @@ def is_instance(cls, item):
431431
or (isinstance(item, type) and issubclass(item, cls))
432432

433433

434+
class OPGeneric:
435+
def __init__(self, matcher: Callable=None):
436+
if matcher is not None:
437+
self.matcher = matcher
438+
439+
def match(self, op) -> bool:
440+
return self.matcher(op)
441+
442+
@classmethod
443+
def is_instance(cls, item):
444+
# accept objects that are instances of this class
445+
# or other classes that are subclasses
446+
return isinstance(item, cls) \
447+
or (isinstance(item, type) and issubclass(item, cls))
448+
434449
OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65))
450+
OP_ANYSEGWIT_VERSION = OPGeneric(lambda x: x in list(range(opcodes.OP_1, opcodes.OP_16 + 1)))
435451

436452
SCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160,
437453
OPPushDataGeneric(lambda x: x == 20),
@@ -440,6 +456,22 @@ def is_instance(cls, item):
440456
SCRIPTPUBKEY_TEMPLATE_WITNESS_V0 = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
441457
SCRIPTPUBKEY_TEMPLATE_P2WPKH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 20)]
442458
SCRIPTPUBKEY_TEMPLATE_P2WSH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 32)]
459+
SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT = [OP_ANYSEGWIT_VERSION, OPPushDataGeneric(lambda x: x in list(range(2, 40 + 1)))]
460+
461+
462+
def check_scriptpubkey_template_and_dust(scriptpubkey, amount: Optional[int]):
463+
if match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2PKH):
464+
dust_limit = bitcoin.DUST_LIMIT_P2PKH
465+
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2SH):
466+
dust_limit = bitcoin.DUST_LIMIT_P2SH
467+
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WSH):
468+
dust_limit = bitcoin.DUST_LIMIT_P2WSH
469+
elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WPKH):
470+
dust_limit = bitcoin.DUST_LIMIT_P2WPKH
471+
else:
472+
raise Exception(f'scriptpubkey does not conform to any template: {scriptpubkey.hex()}')
473+
if amount < dust_limit:
474+
raise Exception(f'amount ({amount}) is below dust limit for scriptpubkey type ({dust_limit})')
443475

444476

445477
def match_script_against_template(script, template) -> bool:
@@ -459,6 +491,8 @@ def match_script_against_template(script, template) -> bool:
459491
script_item = script[i]
460492
if OPPushDataGeneric.is_instance(template_item) and template_item.check_data_len(script_item[0]):
461493
continue
494+
if OPGeneric.is_instance(template_item) and template_item.match(script_item[0]):
495+
continue
462496
if template_item != script_item[0]:
463497
return False
464498
return True

0 commit comments

Comments
 (0)