Skip to content

Commit f45142a

Browse files
committed
Migrate channels to taproot during splices
We add an optional "commitment format" TLV to SpliceInit/SpliceAck which allows us to migrate channels to taproot. This migration is safe as the previous funding tx will be spent, which invalidates oiur peer's old (and now revoked) commitment transactions.
1 parent ca6dcd7 commit f45142a

File tree

11 files changed

+99
-58
lines changed

11 files changed

+99
-58
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator._
4747
import fr.acinq.eclair.payment.send.{ClearRecipient, OfferPayment, PaymentIdentifier}
4848
import fr.acinq.eclair.router.Router
4949
import fr.acinq.eclair.router.Router._
50+
import fr.acinq.eclair.transactions.Transactions.CommitmentFormat
5051
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
5152
import fr.acinq.eclair.wire.protocol._
5253
import grizzled.slf4j.Logging
@@ -96,9 +97,9 @@ trait Eclair {
9697

9798
def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
9899

99-
def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
100+
def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], commitmentFormat_opt: Option[CommitmentFormat])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
100101

101-
def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
102+
def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], commitmentFormat_opt: Option[CommitmentFormat])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
102103

103104
def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
104105

@@ -260,15 +261,15 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
260261
)
261262
}
262263

263-
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
264+
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], commitmentFormat_opt: Option[CommitmentFormat])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
264265
val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))
265266
sendToChannelTyped(
266267
channel = Left(channelId),
267-
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None)
268+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None, commitmentFormat_opt = commitmentFormat_opt)
268269
)
269270
}
270271

271-
override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
272+
override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], commitmentFormat_opt: Option[CommitmentFormat])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
272273
val script = scriptOrAddress match {
273274
case Left(script) => script
274275
case Right(address) => addressToPublicKeyScript(this.appKit.nodeParams.chainHash, address) match {
@@ -279,7 +280,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
279280
val spliceOut = SpliceOut(amount = amountOut, scriptPubKey = script)
280281
sendToChannelTyped(
281282
channel = Left(channelId),
282-
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None)
283+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None, commitmentFormat_opt = commitmentFormat_opt)
283284
)
284285
}
285286

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ sealed trait ChannelFundingCommand extends Command {
242242
}
243243
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
244244
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
245-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends ChannelFundingCommand {
245+
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding], commitmentFormat_opt:Option[CommitmentFormat]) extends ChannelFundingCommand {
246246
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
247247
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
248248
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,10 +1112,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11121112
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage)
11131113
} else {
11141114
val parentCommitment = d.commitments.latest.commitment
1115-
val commitmentFormat = parentCommitment.commitmentFormat
11161115
val localFundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey
1117-
val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, commitmentFormat).pubkeyScript
1118-
val sharedInput = d.commitments.latest.commitmentFormat match {
1116+
val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript
1117+
val sharedInput = parentCommitment.commitmentFormat match {
11191118
case _: SimpleTaprootChannelCommitmentFormat => Musig2Input(channelKeys, parentCommitment)
11201119
case _ => Multisig2of2Input(channelKeys, parentCommitment)
11211120
}
@@ -1131,8 +1130,10 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11311130
pushAmount = 0.msat,
11321131
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
11331132
willFund_opt = willFund_opt.map(_.willFund),
1134-
feeCreditUsed_opt = msg.useFeeCredit_opt
1133+
feeCreditUsed_opt = msg.useFeeCredit_opt,
1134+
commitmentFormat_opt = msg.commitmentFormat_opt
11351135
)
1136+
val commitmentFormat = msg.commitmentFormat_opt.getOrElse(parentCommitment.commitmentFormat)
11361137
val fundingParams = InteractiveTxParams(
11371138
channelId = d.channelId,
11381139
isInitiator = false,
@@ -1181,11 +1182,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11811182
case SpliceStatus.SpliceRequested(cmd, spliceInit) =>
11821183
log.info("our peer accepted our splice request and will contribute {} to the funding transaction", msg.fundingContribution)
11831184
val parentCommitment = d.commitments.latest.commitment
1184-
val commitmentFormat = parentCommitment.commitmentFormat
1185-
val sharedInput = d.commitments.latest.commitmentFormat match {
1186-
case _: SimpleTaprootChannelCommitmentFormat => Musig2Input(channelKeys, parentCommitment)
1187-
case _ => Multisig2of2Input(channelKeys, parentCommitment)
1188-
}
1185+
val sharedInput = SharedFundingInput(channelKeys, parentCommitment)
1186+
val nextCommitmentFormat = msg.commitmentFormat_opt.getOrElse(parentCommitment.commitmentFormat)
11891187
val fundingParams = InteractiveTxParams(
11901188
channelId = d.channelId,
11911189
isInitiator = true,
@@ -1194,13 +1192,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11941192
sharedInput_opt = Some(sharedInput),
11951193
remoteFundingPubKey = msg.fundingPubKey,
11961194
localOutputs = cmd.spliceOutputs,
1197-
commitmentFormat = commitmentFormat,
1195+
commitmentFormat = nextCommitmentFormat,
11981196
lockTime = spliceInit.lockTime,
11991197
dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit),
12001198
targetFeerate = spliceInit.feerate,
12011199
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs)
12021200
)
1203-
val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, commitmentFormat).pubkeyScript
1201+
val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript
12041202
LiquidityAds.validateRemoteFunding(spliceInit.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, isChannelCreation = false, msg.willFund_opt) match {
12051203
case Left(t) =>
12061204
log.info("rejecting splice attempt: invalid liquidity ads response ({})", t.getMessage)
@@ -3481,7 +3479,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
34813479
fundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey,
34823480
pushAmount = cmd.pushAmount,
34833481
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
3484-
requestFunding_opt = cmd.requestFunding_opt
3482+
requestFunding_opt = cmd.requestFunding_opt,
3483+
commitmentFormat_opt = cmd.commitmentFormat_opt
34853484
)
34863485
Right(spliceInit)
34873486
}

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ object InteractiveTxBuilder {
110110
// @formatter:on
111111
}
112112

113+
object SharedFundingInput {
114+
def apply(channelKeys: ChannelKeys, commitment: Commitment): SharedFundingInput = commitment.commitmentFormat match {
115+
case _: SimpleTaprootChannelCommitmentFormat => Musig2Input(channelKeys, commitment)
116+
case _ => Multisig2of2Input(channelKeys, commitment)
117+
}
118+
}
119+
113120
case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput {
114121
override val weight: Int = 388
115122

@@ -482,7 +489,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
482489

483490
private val log = context.log
484491
private val localFundingKey: PrivateKey = channelKeys.fundingKey(purpose.fundingTxIndex)
485-
private val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, channelParams.channelFeatures.commitmentFormat).pubkeyScript
492+
private val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript
486493
private val remoteNodeId = channelParams.remoteParams.nodeId
487494
private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match {
488495
case rbf: FundingTxRbf => rbf.previousTransactions
@@ -563,12 +570,12 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
563570
val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = None)
564571
receive(next)
565572
case Nil =>
566-
val txComplete = channelParams.channelFeatures.commitmentFormat match {
573+
val txComplete = fundingParams.commitmentFormat match {
567574
case _: SimpleTaprootChannelCommitmentFormat =>
568575
// funding nonces are used to sign shared inputs (funding transactions), are randomized and are local to this interactive session
569576
val fundingNonce_opt = (session.remoteInputs ++ session.localInputs).sortBy(_.serialId).collectFirst {
570-
case i: Input.Shared => session.secretNonces.get(i.serialId).map(_.publicNonce).getOrElse(throw new RuntimeException("missing secret nonce"))
571-
}
577+
case i: Input.Shared => session.secretNonces.get(i.serialId).map(_.publicNonce) //.getOrElse(throw new RuntimeException("missing secret nonce"))
578+
}.flatten
572579
// commit nonces are used to sign commit transactions and are sent to our peer. Once this session complete the last one we've received becomes "their next remote nonce"
573580
validateTx(session).map(_.buildUnsignedTx().txid).map { fundingTxId =>
574581
TxComplete(fundingParams.channelId,

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
2020
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId}
2121
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
2222
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes}
23+
import fr.acinq.eclair.transactions.Transactions.CommitmentFormat
2324
import fr.acinq.eclair.wire.protocol.ChannelTlv.{nextLocalNonceTlvCodec, nextLocalNoncesTlvCodec}
2425
import fr.acinq.eclair.wire.protocol.CommonCodecs._
2526
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tmillisatoshi}
@@ -72,6 +73,8 @@ object ChannelTlv {
7273

7374
val requestFundingCodec: Codec[RequestFundingTlv] = tlvField(LiquidityAds.Codecs.requestFunding)
7475

76+
case class UpgradeCommitmentFormatTlv(commitmentFormat: CommitmentFormat) extends SpliceInitTlv with SpliceAckTlv
77+
7578
/** Accept inbound liquidity request. */
7679
case class ProvideFundingTlv(willFund: LiquidityAds.WillFund) extends AcceptDualFundedChannelTlv with TxAckRbfTlv with SpliceAckTlv
7780

@@ -181,6 +184,7 @@ object SpliceInitTlv {
181184
// We use a temporary TLV while the spec is being reviewed.
182185
.typecase(UInt64(1339), requestFundingCodec)
183186
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
187+
.typecase(UInt64(0x47000011), tlvField(commitmentFormatCodec.as[UpgradeCommitmentFormatTlv]))
184188
)
185189
}
186190

@@ -194,6 +198,7 @@ object SpliceAckTlv {
194198
.typecase(UInt64(1339), provideFundingCodec)
195199
.typecase(UInt64(41042), feeCreditUsedCodec)
196200
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
201+
.typecase(UInt64(0x47000011), tlvField(commitmentFormatCodec.as[UpgradeCommitmentFormatTlv]))
197202
)
198203
}
199204

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2323
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
2424
import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases}
2525
import fr.acinq.eclair.crypto.Mac32
26+
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
2627
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId}
2728
import fr.acinq.secp256k1.Secp256k1
2829
import org.apache.commons.codec.binary.Base32
@@ -199,6 +200,13 @@ object CommonCodecs {
199200

200201
val initFeaturesCodec: Codec[Features[InitFeature]] = lengthPrefixedFeaturesCodec.xmap[Features[InitFeature]](_.initFeatures(), _.unscoped())
201202

203+
val commitmentFormatCodec: Codec[CommitmentFormat] = discriminated[CommitmentFormat].by(uint8)
204+
.typecase(0x01, provide(DefaultCommitmentFormat))
205+
.typecase(0x02, provide(UnsafeLegacyAnchorOutputsCommitmentFormat))
206+
.typecase(0x03, provide(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
207+
.typecase(0x04, provide(LegacySimpleTaprootChannelCommitmentFormat))
208+
.typecase(0x05, provide(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat))
209+
202210
/** Returns the same codec, that catches all exceptions when decoding. */
203211
def catchAllCodec[T](codec: Codec[T]): Codec[T] = Codec[T](
204212
(o: T) => codec.encode(o),

0 commit comments

Comments
 (0)