Skip to content

Commit 129df36

Browse files
authored
Add backwards-compatible parts of the official splicing protocol (#3261)
* Add `funding_txid` to `commit_sig` In lightning/bolts#1160 we add a TLV field to `commit_sig` messages to let the receiver know to which `funding_txid` this signature applies. This is more resilient than relying on the order of the `commit_sig` messages in the batch. This is an odd TLV, so we can start writing it right now without creating compatibility issues. We also slightly refactor existing code to make it easier to introduce a backwards-compat layer when migrating to the official splicing. We also increase the default number of RBF attempts allowed. * Insert a `start_batch` message during splices In lightning/bolts#1160, we introduce a message to let our peer know how many `commit_sig` messages they will receive and treat them as a batch. This replaces our previous version that did something similar, but by adding a batch TLV in every `commit_sig` message we send. We currently do both: we keep inserting the experimental batch TLV, and we start by sending a `start_batch` message (with the same information). Since it is an odd message (127), it should be safely ignored if our peer doesn't understand it.
1 parent 6336d80 commit 129df36

12 files changed

Lines changed: 309 additions & 51 deletions

File tree

eclair-core/src/main/resources/reference.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ eclair {
109109
funding {
110110
// Each RBF attempt adds more data that we need to store and process, so we want to limit our peers to a reasonable use of RBF.
111111
remote-rbf-limits {
112-
max-attempts = 5 // maximum number of RBF attempts our peer is allowed to make
112+
max-attempts = 10 // maximum number of RBF attempts our peer is allowed to make
113113
attempt-delta-blocks = 6 // minimum number of blocks between RBF attempts
114114
}
115115
// Duration after which we abort a channel creation. If our peer seems unresponsive and doesn't complete the

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,14 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePer
210210
commitmentFormat match {
211211
case _: SegwitV0CommitmentFormat =>
212212
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
213-
Right(CommitSig(channelParams.channelId, sig, htlcSigs.toList, batchSize))
213+
Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, sig, htlcSigs.toList, batchSize))
214214
case _: SimpleTaprootChannelCommitmentFormat =>
215215
remoteNonce_opt match {
216216
case Some(remoteNonce) =>
217217
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, commitInput.outPoint.txid)
218218
remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
219219
case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
220-
case Right(psig) => Right(CommitSig(channelParams.channelId, psig, htlcSigs.toList, batchSize))
220+
case Right(psig) => Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, psig, htlcSigs.toList, batchSize))
221221
}
222222
case None => Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
223223
}
@@ -650,7 +650,7 @@ case class Commitment(fundingTxIndex: Long,
650650
case None => return Left(MissingCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1))
651651
}
652652
}
653-
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, batchSize)
653+
val commitSig = CommitSig(params.channelId, fundingTxId, sig, htlcSigs.toList, batchSize)
654654
val nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)
655655
Right((copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig))
656656
}
@@ -1089,9 +1089,11 @@ case class Commitments(channelParams: ChannelParams,
10891089
case _: CommitSig if active.size > 1 => return Left(CommitSigCountMismatch(channelId, active.size, 1))
10901090
case commitSig: CommitSig => Seq(commitSig)
10911091
}
1092-
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
10931092
val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex + 1)
1094-
val active1 = active.zip(sigs).map { case (commitment, commit) =>
1093+
val active1 = active.zipWithIndex.map { case (commitment, idx) =>
1094+
// If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first).
1095+
// This matches the behavior of peers who only support the experimental version of splicing.
1096+
val commit = sigs.find(_.fundingTxId_opt.contains(commitment.fundingTxId)).getOrElse(sigs(idx))
10951097
commitment.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit) match {
10961098
case Left(f) => return Left(f)
10971099
case Right(commitment1) => commitment1

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
949949
case Right(localSigOfRemoteTx) =>
950950
val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList
951951
log.info(s"built remote commit number=${purpose.remoteCommitIndex} toLocalMsat=${remoteSpec.toLocal.toLong} toRemoteMsat=${remoteSpec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${remoteSpec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=${fundingTx.txid}", remoteSpec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), remoteSpec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","))
952-
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, batchSize = 1)
952+
val localCommitSig = CommitSig(fundingParams.channelId, fundingTx.txid, localSigOfRemoteTx, htlcSignatures, batchSize = 1)
953953
val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid)
954954
val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint)
955955
signFundingTx(completeTx, remoteFundingNonce_opt, remoteCommitNonces_opt.map(_.nextCommitNonce), localCommitSig, localCommit, remoteCommit)

eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,10 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
208208

209209
case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message
210210
msg match {
211-
case batch: CommitSigBatch => batch.messages.foreach(msg => d.transport forward msg)
211+
case batch: CommitSigBatch =>
212+
// We insert a start_batch message to let our peer know how many commit_sig they will receive.
213+
d.transport forward StartBatch.commitSigBatch(batch.channelId, batch.batchSize)
214+
batch.messages.foreach(msg => d.transport forward msg)
212215
case msg => d.transport forward msg
213216
}
214217
msg match {
@@ -347,8 +350,51 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
347350
// We immediately forward messages to the peer, unless they are part of a batch, in which case we wait to
348351
// receive the whole batch before forwarding.
349352
msg match {
353+
case msg: StartBatch =>
354+
if (!msg.messageType_opt.contains(132)) {
355+
log.debug("ignoring start_batch: we only support batching commit_sig messages")
356+
d.transport ! Warning(msg.channelId, "invalid start_batch message: we only support batching commit_sig messages")
357+
stay()
358+
} else if (msg.batchSize > 20) {
359+
log.debug("ignoring start_batch with batch_size = {} > 20", msg.batchSize)
360+
d.transport ! Warning(msg.channelId, "invalid start_batch message: batch_size must not be greater than 20")
361+
stay()
362+
} else {
363+
log.debug("starting commit_sig batch of size {} for channel_id={}", msg.batchSize, msg.channelId)
364+
d.commitSigBatch_opt match {
365+
case Some(pending) if pending.received.nonEmpty =>
366+
log.warning("starting batch with incomplete previous batch ({}/{} received)", pending.received.size, pending.batchSize)
367+
// This is a spec violation from our peer: this will likely lead to a force-close.
368+
d.transport ! Warning(msg.channelId, "invalid start_batch message: the previous batch is not done yet")
369+
d.peer ! CommitSigBatch(pending.received)
370+
case _ => ()
371+
}
372+
stay() using d.copy(commitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, msg.batchSize, Nil)))
373+
}
374+
case msg: HasChannelId if d.commitSigBatch_opt.nonEmpty =>
375+
// We only support batches of commit_sig messages: other messages will simply be relayed individually.
376+
val pending = d.commitSigBatch_opt.get
377+
msg match {
378+
case msg: CommitSig if msg.channelId == pending.channelId =>
379+
val received1 = pending.received :+ msg
380+
if (received1.size == pending.batchSize) {
381+
log.debug("received last commit_sig in batch for channel_id={}", msg.channelId)
382+
d.peer ! CommitSigBatch(received1)
383+
stay() using d.copy(commitSigBatch_opt = None)
384+
} else {
385+
log.debug("received commit_sig {}/{} in batch for channel_id={}", received1.size, pending.batchSize, msg.channelId)
386+
stay() using d.copy(commitSigBatch_opt = Some(pending.copy(received = received1)))
387+
}
388+
case _ =>
389+
log.warning("received {} as part of a batch: we don't support batching that kind of messages", msg.getClass.getSimpleName)
390+
if (pending.received.nonEmpty) d.peer ! CommitSigBatch(pending.received)
391+
d.peer ! msg
392+
stay() using d.copy(commitSigBatch_opt = None)
393+
}
350394
case msg: CommitSig =>
351-
msg.tlvStream.get[CommitSigTlv.BatchTlv].map(_.size) match {
395+
// We keep supporting the experimental version of splicing that older Phoenix wallets use.
396+
// Once we're confident that enough Phoenix users have upgraded, we should remove this branch.
397+
msg.tlvStream.get[CommitSigTlv.ExperimentalBatchTlv].map(_.size) match {
352398
case Some(batchSize) if batchSize > 25 =>
353399
log.warning("received legacy batch of commit_sig exceeding our threshold ({} > 25), processing messages individually", batchSize)
354400
// We don't want peers to be able to exhaust our memory by sending batches of dummy messages that we keep in RAM.
@@ -612,6 +658,7 @@ object PeerConnection {
612658
gossipTimestampFilter: Option[GossipTimestampFilter] = None,
613659
behavior: Behavior = Behavior(),
614660
expectedPong_opt: Option[ExpectedPong] = None,
661+
commitSigBatch_opt: Option[PendingCommitSigBatch] = None,
615662
legacyCommitSigBatch_opt: Option[PendingCommitSigBatch] = None,
616663
isPersistent: Boolean) extends Data with HasTransport
617664

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,16 @@ sealed trait ChannelReestablishTlv extends Tlv
255255

256256
object ChannelReestablishTlv {
257257

258+
/**
259+
* When disconnected in the middle of an interactive-tx session, this field is used to request a retransmission of
260+
* [[TxSignatures]] for the given [[txId]].
261+
*/
258262
case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
263+
264+
/** The txid of the last [[ChannelReady]] or [[SpliceLocked]] message received before disconnecting, if any. */
259265
case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
266+
267+
/** The txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel. */
260268
case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
261269

262270
/**
@@ -395,3 +403,13 @@ object ClosingSigTlv {
395403
)
396404
}
397405

406+
sealed trait StartBatchTlv extends Tlv
407+
408+
object StartBatchTlv {
409+
/** Type of [[LightningMessage]] that is included in the batch, when batching a single message type. */
410+
case class MessageType(tag: Int) extends StartBatchTlv
411+
412+
val startBatchTlvCodec: Codec[TlvStream[StartBatchTlv]] = tlvStream(discriminated[StartBatchTlv].by(varint)
413+
.typecase(UInt64(1), tlvField(uint16.as[MessageType]))
414+
)
415+
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,23 @@ sealed trait CommitSigTlv extends Tlv
9696

9797
object CommitSigTlv {
9898

99-
/** @param size the number of [[CommitSig]] messages in the batch */
100-
case class BatchTlv(size: Int) extends CommitSigTlv
99+
/**
100+
* While a splice is ongoing and not locked, we have multiple valid commitments.
101+
* We send one [[CommitSig]] message for each valid commitment: this field maps it to the corresponding funding transaction.
102+
*
103+
* @param txId the funding transaction spent by this commitment.
104+
*/
105+
case class FundingTx(txId: TxId) extends CommitSigTlv
101106

102-
object BatchTlv {
103-
val codec: Codec[BatchTlv] = tlvField(tu16)
104-
}
107+
private val fundingTxTlv: Codec[FundingTx] = tlvField(txIdAsHash)
108+
109+
/**
110+
* The experimental version of splicing included the number of [[CommitSig]] messages in the batch.
111+
* This TLV can be removed once Phoenix users have upgraded to the official version of splicing and use the [[StartBatch]] message.
112+
*/
113+
case class ExperimentalBatchTlv(size: Int) extends CommitSigTlv
114+
115+
private val experimentalBatchTlv: Codec[ExperimentalBatchTlv] = tlvField(tu16)
105116

106117
/** Partial signature signature for the current commitment transaction, along with the signing nonce used (when using taproot channels). */
107118
case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends CommitSigTlv
@@ -111,8 +122,9 @@ object CommitSigTlv {
111122
}
112123

113124
val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint)
125+
.typecase(UInt64(1), fundingTxTlv)
114126
.typecase(UInt64(2), PartialSignatureWithNonceTlv.codec)
115-
.typecase(UInt64(0x47010005), BatchTlv.codec)
127+
.typecase(UInt64(0x47010005), experimentalBatchTlv)
116128
)
117129

118130
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ object LightningMessageCodecs {
244244
("lockTime" | uint32) ::
245245
("tlvStream" | ClosingSigTlv.closingSigTlvCodec)).as[ClosingSig]
246246

247+
val startBatchCodec: Codec[StartBatch] = (
248+
("channelId" | bytes32) ::
249+
("batchSize" | uint16) ::
250+
("tlvStream" | StartBatchTlv.startBatchTlvCodec)).as[StartBatch]
251+
247252
val updateAddHtlcCodec: Codec[UpdateAddHtlc] = (
248253
("channelId" | bytes32) ::
249254
("id" | uint64overflow) ::
@@ -525,6 +530,7 @@ object LightningMessageCodecs {
525530
.typecase(72, txInitRbfCodec)
526531
.typecase(73, txAckRbfCodec)
527532
.typecase(74, txAbortCodec)
533+
.typecase(127, startBatchCodec)
528534
.typecase(128, updateAddHtlcCodec)
529535
.typecase(130, updateFulfillHtlcCodec)
530536
.typecase(131, updateFailHtlcCodec)

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ case class TxAddInput(channelId: ByteVector32,
102102

103103
object TxAddInput {
104104
def apply(channelId: ByteVector32, serialId: UInt64, sharedInput: OutPoint, sequence: Long): TxAddInput = {
105-
TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(TxAddInputTlv.SharedInputTxId(sharedInput.txid)))
105+
val tlvs = Set[TxAddInputTlv](
106+
TxAddInputTlv.SharedInputTxId(sharedInput.txid),
107+
)
108+
TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(tlvs))
106109
}
107110
}
108111

@@ -146,12 +149,11 @@ case class TxSignatures(channelId: ByteVector32,
146149

147150
object TxSignatures {
148151
def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = {
149-
val tlvs: Set[TxSignaturesTlv] = Set(
150-
previousFundingSig_opt.map {
151-
case IndividualSignature(sig) => TxSignaturesTlv.PreviousFundingTxSig(sig)
152-
case partialSig: PartialSignatureWithNonce => TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig)
153-
}
154-
).flatten
152+
val tlvs: Set[TxSignaturesTlv] = previousFundingSig_opt match {
153+
case Some(IndividualSignature(sig)) => Set(TxSignaturesTlv.PreviousFundingTxSig(sig))
154+
case Some(partialSig: PartialSignatureWithNonce) => Set(TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig))
155+
case None => Set.empty
156+
}
155157
TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs))
156158
}
157159
}
@@ -476,6 +478,15 @@ case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, c
476478
val nextCloseeNonce_opt: Option[IndividualNonce] = tlvStream.get[ClosingSigTlv.NextCloseeNonce].map(_.nonce)
477479
}
478480

481+
/** This message is used to indicate that the next [[batchSize]] messages form a single logical message. */
482+
case class StartBatch(channelId: ByteVector32, batchSize: Int, tlvStream: TlvStream[StartBatchTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
483+
val messageType_opt: Option[Long] = tlvStream.get[StartBatchTlv.MessageType].map(_.tag)
484+
}
485+
486+
object StartBatch {
487+
def commitSigBatch(channelId: ByteVector32, batchSize: Int): StartBatch = StartBatch(channelId, batchSize, TlvStream(StartBatchTlv.MessageType(132)))
488+
}
489+
479490
case class UpdateAddHtlc(channelId: ByteVector32,
480491
id: Long,
481492
amountMsat: MilliSatoshi,
@@ -545,19 +556,21 @@ case class CommitSig(channelId: ByteVector32,
545556
signature: IndividualSignature,
546557
htlcSignatures: List[ByteVector64],
547558
tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs {
559+
val fundingTxId_opt: Option[TxId] = tlvStream.get[CommitSigTlv.FundingTx].map(_.txId)
548560
val partialSignature_opt: Option[PartialSignatureWithNonce] = tlvStream.get[CommitSigTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce)
549561
val sigOrPartialSig: ChannelSpendSignature = partialSignature_opt.getOrElse(signature)
550562
}
551563

552564
object CommitSig {
553-
def apply(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = {
565+
def apply(channelId: ByteVector32, fundingTxId: TxId, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = {
554566
val (individualSig, partialSig_opt) = signature match {
555567
case sig: IndividualSignature => (sig, None)
556568
case psig: PartialSignatureWithNonce => (IndividualSignature(ByteVector64.Zeroes), Some(psig))
557569
}
558570
val tlvs = Set(
559-
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None,
560-
partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_))
571+
Some(CommitSigTlv.FundingTx(fundingTxId)),
572+
partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_)),
573+
if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None,
561574
).flatten[CommitSigTlv]
562575
CommitSig(channelId, individualSig, htlcSignatures, TlvStream(tlvs))
563576
}

eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3030,7 +3030,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
30303030
bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete])
30313031
// Alice <-- commit_sig --- Bob
30323032
val successA1 = alice2bob.expectMsgType[Succeeded]
3033-
val invalidCommitSig = CommitSig(params.channelId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.commitNonces_opt.get.commitNonce), Nil, batchSize = 1)
3033+
val invalidCommitSig = CommitSig(params.channelId, successA1.signingSession.fundingTxId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.commitNonces_opt.get.commitNonce), Nil, batchSize = 1)
30343034
val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging)
30353035
assert(error.isInstanceOf[InvalidCommitmentSignature])
30363036
}

0 commit comments

Comments
 (0)