Skip to content

Commit d59f1ea

Browse files
committed
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.
1 parent 656e503 commit d59f1ea

File tree

11 files changed

+88
-50
lines changed

11 files changed

+88
-50
lines changed

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
@@ -933,7 +933,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
933933
case Right(localSigOfRemoteTx) =>
934934
val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList
935935
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(","))
936-
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, batchSize = 1)
936+
val localCommitSig = CommitSig(fundingParams.channelId, fundingTx.txid, localSigOfRemoteTx, htlcSignatures, batchSize = 1)
937937
val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid)
938938
val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint)
939939
signFundingTx(completeTx, remoteFundingNonce_opt, remoteCommitNonces_opt.map(_.nextCommitNonce), localCommitSig, localCommit, remoteCommit)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
350350
// receive the whole batch before forwarding.
351351
msg match {
352352
case msg: CommitSig =>
353-
msg.tlvStream.get[CommitSigTlv.BatchTlv].map(_.size) match {
353+
msg.tlvStream.get[CommitSigTlv.ExperimentalBatchTlv].map(_.size) match {
354354
case Some(batchSize) if batchSize > 25 =>
355355
log.warning("received legacy batch of commit_sig exceeding our threshold ({} > 25), processing messages individually", batchSize)
356356
// We don't want peers to be able to exhaust our memory by sending batches of dummy messages that we keep in RAM.

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

Lines changed: 8 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
/**

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.
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/LightningMessageTypes.scala

Lines changed: 14 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
}
@@ -545,19 +547,21 @@ case class CommitSig(channelId: ByteVector32,
545547
signature: IndividualSignature,
546548
htlcSignatures: List[ByteVector64],
547549
tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs {
550+
val fundingTxId_opt: Option[TxId] = tlvStream.get[CommitSigTlv.FundingTx].map(_.txId)
548551
val partialSignature_opt: Option[PartialSignatureWithNonce] = tlvStream.get[CommitSigTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce)
549552
val sigOrPartialSig: ChannelSpendSignature = partialSignature_opt.getOrElse(signature)
550553
}
551554

552555
object CommitSig {
553-
def apply(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = {
556+
def apply(channelId: ByteVector32, fundingTxId: TxId, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = {
554557
val (individualSig, partialSig_opt) = signature match {
555558
case sig: IndividualSignature => (sig, None)
556559
case psig: PartialSignatureWithNonce => (IndividualSignature(ByteVector64.Zeroes), Some(psig))
557560
}
558561
val tlvs = Set(
559-
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None,
560-
partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_))
562+
Some(CommitSigTlv.FundingTx(fundingTxId)),
563+
partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_)),
564+
if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None,
561565
).flatten[CommitSigTlv]
562566
CommitSig(channelId, individualSig, htlcSignatures, TlvStream(tlvs))
563567
}

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
@@ -2955,7 +2955,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
29552955
bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete])
29562956
// Alice <-- commit_sig --- Bob
29572957
val successA1 = alice2bob.expectMsgType[Succeeded]
2958-
val invalidCommitSig = CommitSig(params.channelId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.commitNonces_opt.get.commitNonce), Nil, batchSize = 1)
2958+
val invalidCommitSig = CommitSig(params.channelId, successA1.signingSession.fundingTxId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.commitNonces_opt.get.commitNonce), Nil, batchSize = 1)
29592959
val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging)
29602960
assert(error.isInstanceOf[InvalidCommitmentSignature])
29612961
}

0 commit comments

Comments
 (0)