Fix encoding of channel type TLV in splice_init/splice_ack (#3178)
What changed, and why it matters
This commit fixes a wire-encoding bug in Eclair's Lightning splicing messages. The channel type field was being wrapped twice, which produced an incorrect byte layout. The fix removes the extra wrapper so the messages encode and decode correctly. It is a protocol-compatibility bug rather than a direct theft-of-funds vulnerability, but it could cause peers to reject or misinterpret splice messages.
Treat as a protocol correctness fix. Review whether any deployed version exchanged malformed splice_init/splice_ack channel-type TLVs with peers, and consider compatibility handling or a coordinated upgrade if the feature is live. No immediate emergency patch is indicated for theft of funds.
Security signals we found
Wire protocol encoding bug in TLV codec registration
Double length-prefix wrapping of channel type field
Potential peer interoperability failure for splice messages carrying channel type
Test vectors added for taproot channel type in splice_init/splice_ack
Evidence from the diff
In ChannelTlv.scala, the SpliceInitTlv and SpliceAckTlv TLV registries previously registered the channel type TLV (type 0x47000011) using tlvField(channelTypeCodec.as[ChannelTypeTlv]). Because channelTypeCodec is already a TLV field codec, the extra tlvField() added a redundant length prefix, corrupting the encoding. The patch changes both registrations to channelTypeCodec.as[ChannelTypeTlv]. New test vectors in LightningMessageCodecsSpec.scala exercise SimpleTaprootChannelsPhoenix in SpliceInit and SpliceAck and assert the expected serialized bytes.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scalaSpliceInitTlv channel type TLV registrationSpliceAckTlv channel type TLV registrationLightning splicing protocol messagesInspect captured patch +5 / −3
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
index 13e0b76..e0a4440 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
@@ -183,7 +183,7 @@ object SpliceInitTlv {
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), requestFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
- .typecase(UInt64(0x47000011), tlvField(channelTypeCodec.as[ChannelTypeTlv]))
+ .typecase(UInt64(0x47000011), channelTypeCodec.as[ChannelTypeTlv])
)
}
@@ -197,7 +197,7 @@ object SpliceAckTlv {
.typecase(UInt64(1339), provideFundingCodec)
.typecase(UInt64(41042), feeCreditUsedCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
- .typecase(UInt64(0x47000011), tlvField(channelTypeCodec.as[ChannelTypeTlv]))
+ .typecase(UInt64(0x47000011), channelTypeCodec.as[ChannelTypeTlv])
)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
index dbc563c..13140c8 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
@@ -26,6 +26,7 @@ import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce}
+import fr.acinq.eclair.channel.ChannelTypes.SimpleTaprootChannelsPhoenix
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.json.JsonSerializers
import fr.acinq.eclair.reputation.Reputation
@@ -424,6 +425,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000",
+ SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000",
SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680",
SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
@@ -431,7 +433,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200",
SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1",
- SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, None, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000", SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566",
// @formatter:on
)
testCases.foreach { case (message, bin) =>
Why this scored 45/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.