LNWallet: set OPT_ANCHOR_REQ for peer, rm config.ENABLE_ANCHOR_CHANNELS
What changed, and why it matters
This commit changes Electrum's Lightning wallet so it now requires anchor-style channels by default and removes the old user-facing setting that let people opt out. It replaces the opt-out with a hidden testing-only flag for older channel types. The change is a deliberate protocol hardening step, not a fix for an active exploit, though the commit message notes a follow-up is needed to fully block incoming old-style channels.
Review the follow-up commit that addresses the incoming channel_type check noted in the commit message, and ensure the new TEST_LN_OPEN_SRK_CHANNELS flag is not exposed in normal user interfaces. Consider documenting the rationale for deprecating static-remotekey-only channels.
Security signals we found
Removes a user-configurable setting that could downgrade channel security to pre-anchor (static-remotekey-only) channels
Makes anchor channel support mandatory in advertised LN features
Adds assertions that peers support anchor features before opening channels
Leaves a TODO about rejecting incoming non-anchor channel_type, indicating the change is partial
Evidence from the diff
The patch removes config.ENABLE_ANCHOR_CHANNELS and instead advertises OPTION_ANCHORS_REQ as a mandatory peer feature. Outgoing channel open_channel now asserts the peer supports OPTION_STATIC_REMOTEKEY_OPT and OPTION_ANCHORS_OPT, and uses an anchor channel type unless the new TEST_LN_OPEN_SRK_CHANNELS flag is set. Tests and wallet logic are updated to invert the previous boolean semantics. A TODO remains to fail incoming channels that do not include anchors in their channel_type.
Changed components
electrum/lnpeer.pyelectrum/lnworker.pyelectrum/simple_config.pyelectrum/wallet.pytests/regtest/regtest.shtests/test_lnpeer.pyInspect captured patch +29 / −38
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index f295e5e..1db7e76 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -922,9 +922,6 @@ class Peer(Logger, EventListener):
def is_upfront_shutdown_script(self):
return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
- def use_anchors(self) -> bool:
- return self.features.supports(LnFeatures.OPTION_ANCHORS_OPT)
-
def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
if msg_identifier not in ['accept', 'open']:
raise ValueError("msg_identifier must be either 'accept' or 'open'")
@@ -994,16 +991,18 @@ class Peer(Logger, EventListener):
channel_flags = CF_ANNOUNCE_CHANNEL if public else 0
feerate: Optional[int] = self.lnworker.current_target_feerate_per_kw(
- has_anchors=self.use_anchors()
+ has_anchors=not self.config.TEST_LN_OPEN_SRK_CHANNELS,
)
if feerate is None:
raise NoDynamicFeeEstimates()
# we set a channel type for internal bookkeeping
open_channel_tlvs = {}
- assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
- our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY)
- if self.use_anchors():
- our_channel_type |= ChannelType(ChannelType.OPTION_ANCHORS)
+ assert self.features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
+ assert self.features.supports(LnFeatures.OPTION_ANCHORS_OPT)
+ if self.config.TEST_LN_OPEN_SRK_CHANNELS:
+ our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY)
+ else: # anchors
+ our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY | ChannelType.OPTION_ANCHORS)
if zeroconf:
our_channel_type |= ChannelType(ChannelType.OPTION_ZEROCONF)
# We do not set the option_scid_alias bit in channel_type because LND rejects it.
@@ -1094,8 +1093,6 @@ class Peer(Logger, EventListener):
if their_channel_type is None:
raise Exception("channel_type MUST be present in accept_channel, but missing")
their_channel_type = ChannelType.from_bytes(their_channel_type['type'], byteorder='big').discard_unknown_and_check()
- # if channel_type is set, and channel_type was set in open_channel,
- # and they are not equal types: MUST reject the channel.
if their_channel_type != our_channel_type:
raise Exception("channel_type is not the one that we sent.")
@@ -1245,8 +1242,8 @@ class Peer(Logger, EventListener):
channel_type = open_channel_tlvs.get('channel_type')
if channel_type is None:
raise Exception("channel_type MUST be present in open_channel, but missing")
- # MUST fail the channel if it supports channel_type,
- # channel_type was set, and the type is not suitable.
+ # MUST fail the channel if channel_type is not suitable.
+ # TODO fail if channel_type does not have anchors
else:
channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()
if not channel_type.complies_with_features(self.features):
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index 7cb80d7..b59be13 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -199,6 +199,7 @@ LNWALLET_FEATURES = (
BASE_FEATURES
| LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
| LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
+ | LnFeatures.OPTION_ANCHORS_REQ
| LnFeatures.VAR_ONION_REQ
| LnFeatures.PAYMENT_SECRET_REQ
| LnFeatures.BASIC_MPP_OPT
@@ -1014,8 +1015,6 @@ class LNWallet(Logger):
Logger.__init__(self)
if features is None:
features = LNWALLET_FEATURES
- if self.config.ENABLE_ANCHOR_CHANNELS:
- features |= LnFeatures.OPTION_ANCHORS_OPT
if self.config.OPEN_ZEROCONF_CHANNELS:
features |= LnFeatures.OPTION_ZEROCONF_OPT
if self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS or self.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS:
@@ -1597,8 +1596,7 @@ class LNWallet(Logger):
zeroconf: bool = False,
opening_base_fee_msat: Optional[int] = None,
password=None):
- if self.config.ENABLE_ANCHOR_CHANNELS:
- self.wallet.unlock(password)
+ self.wallet.unlock(password)
coins = self.wallet.get_spendable_coins(None)
node_id = peer.pubkey
fee_policy = FeePolicy(self.config.FEE_POLICY)
@@ -1697,6 +1695,7 @@ class LNWallet(Logger):
static_remotekey = None
else: # static_remotekey
assert channel_type & channel_type.OPTION_STATIC_REMOTEKEY
+ #assert self.config.TEST_LN_OPEN_SRK_CHANNELS
wallet = self.wallet
assert wallet.txin_type == 'p2wpkh'
addr = wallet.get_new_sweep_address_for_channel()
@@ -1772,8 +1771,7 @@ class LNWallet(Logger):
coins=coins,
outputs=outputs,
fee_policy=fee_policy,
- # we do not know yet if peer accepts anchors, just assume they do
- is_anchor_channel_opening=self.config.ENABLE_ANCHOR_CHANNELS,
+ is_anchor_channel_opening=not self.config.TEST_LN_OPEN_SRK_CHANNELS,
)
tx.set_rbf(False)
# rm randomness from locktime, as we use the locktime as entropy for deriving the funding_privkey
@@ -3707,10 +3705,7 @@ class LNWallet(Logger):
to sweep funds after a channel has been force closed).
The creation of lightning channels in watching-only wallets
- has been disabled for anchor channels. Note that it is still
- possible to create non-anchor channels, see
- config.ENABLE_ANCHOR_CHANNELS.
-
+ has been disabled for anchor channels.
"""
xpub = self.wallet.get_fingerprint()
backup_bytes = self.create_channel_backup(channel_id).to_bytes()
diff --git a/electrum/simple_config.py b/electrum/simple_config.py
index 56bfca4..4258cbe 100644
--- a/electrum/simple_config.py
+++ b/electrum/simple_config.py
@@ -779,6 +779,7 @@ Warning: setting this to too low will result in lots of payment failures."""),
TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)
TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
+ TEST_LN_OPEN_SRK_CHANNELS = ConfigVar('test_ln_open_srk_channels', default=False, type_=bool)
# fee_policy is a dict: fee_policy_name -> fee_policy_descriptor
FEE_POLICY = ConfigVar('fee_policy.default', default='eta:2', type_=str) # exposed to GUI
@@ -951,8 +952,6 @@ Warning: setting this to too low will result in lots of payment failures."""),
]),
)
- # anchor outputs channels
- ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=True, type_=bool)
# zeroconf channels
OPEN_ZEROCONF_CHANNELS = ConfigVar('open_zeroconf_channels', default=False, type_=bool)
ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 134ee44..93b8985 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -525,7 +525,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# we want static_remotekey to be a wallet address
if not self.txin_type == 'p2wpkh':
return False
- if self.config.ENABLE_ANCHOR_CHANNELS:
+ if not self.config.TEST_LN_OPEN_SRK_CHANNELS: # anchors
if not self.keystore:
return False
if self.keystore.is_watching_only():
diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh
index c0cd959..f5df1e5 100755
--- a/tests/regtest/regtest.sh
+++ b/tests/regtest/regtest.sh
@@ -2,7 +2,7 @@
export HOME=~
set -eu
-TEST_ANCHOR_CHANNELS=True
+TEST_SRK_CHANNELS=False
# alice -> bob -> carol
@@ -167,7 +167,7 @@ if [[ $1 == "init" ]]; then
rm -rf /tmp/$2/
agent="./run_electrum --regtest -D /tmp/$2"
$agent create --offline > /dev/null
- $agent setconfig --offline enable_anchor_channels $TEST_ANCHOR_CHANNELS
+ $agent setconfig --offline test_ln_open_srk_channels $TEST_SRK_CHANNELS
$agent setconfig --offline log_to_file True
$agent setconfig --offline use_gossip True
$agent setconfig --offline server 127.0.0.1:51001:t
@@ -335,9 +335,9 @@ if [[ $1 == "swapserver_forceclose" ]]; then
new_blocks 1
wait_until_spent $funding_txid 0 # alice reveals preimage
new_blocks 1
- if [ $TEST_ANCHOR_CHANNELS = True ] ; then
+ if [ $TEST_SRK_CHANNELS != True ] ; then # anchors
output_index=3 # received_htlc_output in bob's ctx. FIXME index depends on Alice not using MPP
- else
+ else # srk
output_index=1
fi
# wait until Bob finds preimage onchain and uses it to create an htlc_success tx
@@ -419,12 +419,12 @@ if [[ $1 == "lnwatcher_waits_until_fees_go_down" ]]; then
new_blocks 1
wait_until_channel_closed alice
ctx_id=$($alice list_channels | jq -r ".[0].closing_txid")
- if [ $TEST_ANCHOR_CHANNELS = True ] ; then
+ if [ $TEST_SRK_CHANNELS != True ] ; then # anchors
htlc_output_index1=2
htlc_output_index2=3
to_alice_index=4 # Bob's to_remote
wait_until_spent $ctx_id $to_alice_index
- else
+ else # srk
htlc_output_index1=0
htlc_output_index2=1
to_alice_index=2
@@ -669,12 +669,12 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then
$alice load_wallet -w /tmp/alice/regtest/wallets/toxic_wallet
# wait until alice has spent both ctx outputs
echo "alice spends to_local and htlc outputs"
- if [ $TEST_ANCHOR_CHANNELS = True ] ; then
+ if [ $TEST_SRK_CHANNELS != True ] ; then # anchors
# to_local_anchor/to_remote_anchor: 0 and 1 (both are present due to untrimmed htlcs)
# htlc: 2, to_local: 3
wait_until_spent $ctx_id 2
wait_until_spent $ctx_id 3
- else
+ else # srk
# htlc: 0, to_local: 1
wait_until_spent $ctx_id 0
wait_until_spent $ctx_id 1
@@ -717,9 +717,9 @@ if [[ $1 == "watchtower" ]]; then
ctx_id=$($bitcoin_cli sendrawtransaction $ctx)
echo "alice breaches with old ctx:" $ctx_id
echo "watchtower publishes justice transaction"
- if [ $TEST_ANCHOR_CHANNELS = True ] ; then
+ if [ $TEST_SRK_CHANNELS != True ] ; then # anchors
output_index=3
- else
+ else # srk
output_index=1
fi
wait_until_spent $ctx_id $output_index # alice's to_local gets punished
@@ -750,9 +750,9 @@ if [[ $1 == "fw_fail_htlc" ]]; then
sleep 1
new_blocks 150 # cltv before bob can broadcast
# index of htlc
- if [ $TEST_ANCHOR_CHANNELS = True ] ; then
+ if [ $TEST_SRK_CHANNELS != True ] ; then # anchors
output_index=2
- else
+ else # srk
output_index=0
fi
wait_until_spent $ctx_id $output_index
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 6686f6b..0baef73 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -133,7 +133,7 @@ class MockStandardWallet(Standard_Wallet):
def _create_mock_lnwallet(*, name, has_anchors, data_dir: str) -> 'MockLNWallet':
config = SimpleConfig({}, read_user_dir_function=lambda: data_dir)
- config.ENABLE_ANCHOR_CHANNELS = has_anchors
+ config.TEST_LN_OPEN_SRK_CHANNELS = not has_anchors
config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0
network = MockNetwork(config=config)
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.