Don't automatically use `scid_alias` for public channels (#3255)
What changed, and why it matters
This commit fixes a bug where Eclair would automatically enable a feature called `scid_alias` for public Lightning Network channels, even though the Lightning protocol rules (BOLTs) only allow that feature for unannounced (private) channels. Public channels with `scid_alias` could violate protocol rules and cause interoperability or routing problems, though it is not a direct theft-of-funds vulnerability.
Apply the patch to ensure `scid_alias` is only negotiated for unannounced channels. Operators running code from PR #3250 should upgrade. Monitor for any public channels opened with `scid_alias` that may need reconfiguration or closure.
Security signals we found
Protocol compliance bug: using a feature outside its specified scope
Potential channel type mismatch between peers
Risk of public channel announcements containing invalid/restricted feature usage
Could lead to channel open failures or inconsistent routing state
Evidence from the diff
In PR #3250, Eclair began implicitly selecting a channel type when the operator did not explicitly specify one. The helper ChannelTypes.preferredForPublicChannels incorrectly set scidAlias = true whenever both peers supported the scid_alias feature, regardless of whether the channel was announced. The BOLT specification restricts scid_alias to unannounced channels. The patch adds an announceChannel boolean parameter and only enables scid_alias when the channel is not announced. A test is added verifying that announced channels use AnchorOutputsZeroFeeHtlcTx(scidAlias = false) while unannounced channels can use scidAlias = true.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scalaeclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scalaeclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scalaInspect captured patch +12 / −3
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
index 04fda87..311e960 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
@@ -149,9 +149,9 @@ object ChannelTypes {
}
/** Returns our preferred channel type for public channels, if supported by our peer. */
- def preferredForPublicChannels(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Option[SupportedChannelType] = {
+ def preferredForPublicChannels(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): Option[SupportedChannelType] = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) {
- Some(AnchorOutputsZeroFeeHtlcTx(scidAlias = Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias)))
+ Some(AnchorOutputsZeroFeeHtlcTx(scidAlias = !announceChannel && Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias)))
} else {
None
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
index fa0fd9b..9c2e062 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
@@ -108,7 +108,8 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
}
private def sanityCheckInitiator(request: OpenChannelInitiator): Behavior[Command] = {
- val channelType_opt = request.open.channelType_opt.orElse(ChannelTypes.preferredForPublicChannels(request.localFeatures, request.remoteFeatures))
+ val announceChannel = request.open.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags).announceChannel
+ val channelType_opt = request.open.channelType_opt.orElse(ChannelTypes.preferredForPublicChannels(request.localFeatures, request.remoteFeatures, announceChannel = announceChannel))
if (request.open.fundingAmount >= Channel.MAX_FUNDING_WITHOUT_WUMBO && !request.localFeatures.hasFeature(Wumbo)) {
request.replyTo ! OpenChannelResponse.Rejected(s"fundingAmount=${request.open.fundingAmount} is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING_WITHOUT_WUMBO} (see eclair.conf)")
waitForRequest()
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
index 4d1bba9..6f4954b 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
@@ -292,6 +292,14 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val features = Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, ChannelType -> Optional, ScidAlias -> Optional)
val open = Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None, None)
openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, open, features, features)
+ assert(TestConstants.Alice.nodeParams.channelConf.channelFlags.announceChannel)
+ assert(peer.expectMessageType[Peer.SpawnChannelInitiator].channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false))
+ }
+ // If we don't announce the channel, we can use scid_alias.
+ {
+ val features = Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, ChannelType -> Optional, ScidAlias -> Optional)
+ val open = Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, Some(ChannelFlags(announceChannel = false)), None)
+ openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, open, features, features)
assert(peer.expectMessageType[Peer.SpawnChannelInitiator].channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))
}
// If our peer doesn't support anchor outputs, we can't find a satisfying channel type.
Why this scored 37/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.