Skip to content

Commit 144d2a3

Browse files
committed
Use official splice messages
We replace our experimental version of `splice_init`, `splice_ack` and `splice_locked` by their official version. If our peer is using the experimental feature bit, we convert our outgoing messages to use the experimental encoding and incoming messages to the official messages. We also change the TLV fields added to `tx_add_input`, `tx_signatures` and `splice_locked` to match the spec version. We always write both the official and experimental TLV to updated nodes (because the experimental one is odd and will be ignored) but we drop the official TLV if our peer is using the experimental feature, because it won't understand the even TLV field. This guarantees backwards-compatibility with peers who only support the experimental feature.
1 parent 2a3d7d7 commit 144d2a3

14 files changed

Lines changed: 213 additions & 65 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ eclair {
8181
// node that you trust using override-init-features (see below).
8282
option_zeroconf = disabled
8383
keysend = disabled
84+
option_splice = optional
8485
trampoline_payment_prototype = disabled
8586
async_payment_prototype = disabled
8687
on_the_fly_funding = disabled

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,7 @@ object Features {
264264
val mandatory = 28
265265
}
266266

267-
// TODO: this should also extend NodeFeature once the spec is finalized
268-
case object Quiescence extends Feature with InitFeature {
267+
case object Quiescence extends Feature with InitFeature with NodeFeature {
269268
val rfcName = "option_quiesce"
270269
val mandatory = 34
271270
}
@@ -300,6 +299,11 @@ object Features {
300299
val mandatory = 54
301300
}
302301

302+
case object Splicing extends Feature with InitFeature with NodeFeature {
303+
val rfcName = "option_splice"
304+
val mandatory = 62
305+
}
306+
303307
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
304308
// We're not advertising these bits yet in our announcements, clients have to assume support.
305309
// This is why we haven't added them yet to `areSupported`.
@@ -317,12 +321,6 @@ object Features {
317321
val mandatory = 152
318322
}
319323

320-
// TODO: @pm47 custom splices implementation for phoenix, to be replaced once splices is spec-ed (currently reserved here: https://github.com/lightning/bolts/issues/605)
321-
case object SplicePrototype extends Feature with InitFeature {
322-
val rfcName = "splice_prototype"
323-
val mandatory = 154
324-
}
325-
326324
/**
327325
* Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md.
328326
* TODO: add NodeFeature once bLIP is merged.
@@ -363,9 +361,9 @@ object Features {
363361
PaymentMetadata,
364362
ZeroConf,
365363
KeySend,
364+
Splicing,
366365
TrampolinePaymentPrototype,
367366
AsyncPaymentPrototype,
368-
SplicePrototype,
369367
OnTheFlyFunding,
370368
FundingFeeCredit
371369
)
@@ -381,7 +379,6 @@ object Features {
381379
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
382380
KeySend -> (VariableLengthOnion :: Nil),
383381
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
384-
OnTheFlyFunding -> (SplicePrototype :: Nil),
385382
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
386383
)
387384

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -638,9 +638,11 @@ case class Commitment(fundingTxIndex: Long,
638638
log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","))
639639
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)
640640

641-
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set(
642-
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None
643-
).flatten[CommitSigTlv]))
641+
val tlvs = Set(
642+
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize, fundingTxId)) else None,
643+
if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None,
644+
).flatten[CommitSigTlv]
645+
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs))
644646
val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
645647
(copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig)
646648
}
@@ -1021,8 +1023,10 @@ case class Commitments(params: ChannelParams,
10211023
}
10221024
val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig)
10231025
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
1024-
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
1025-
val active1 = active.zip(commits).map { case (commitment, commit) =>
1026+
val active1 = active.zipWithIndex.map { case (commitment, idx) =>
1027+
// If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first).
1028+
// This matches the behavior of peers who only support the experimental version of splicing.
1029+
val commit = commits.find(_.fundingTxId_opt.contains(commitment.fundingTxId)).getOrElse(commits(idx))
10261030
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match {
10271031
case Left(f) => return Left(f)
10281032
case Right(commitment1) => commitment1

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
852852
}
853853

854854
case Event(cmd: CMD_SPLICE, d: DATA_NORMAL) =>
855-
if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype)) {
855+
if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.Splicing)) {
856856
d.spliceStatus match {
857857
case SpliceStatus.NoSplice if d.commitments.params.useQuiescence =>
858858
startSingleTimer(QuiescenceTimeout.toString, QuiescenceTimeout(peer), nodeParams.channelConf.quiescenceTimeout)
@@ -2556,7 +2556,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
25562556
/** For splices we will send one commit_sig per active commitments. */
25572557
private def aggregateSigs(commit: CommitSig): Option[Seq[CommitSig]] = {
25582558
sigStash = sigStash :+ commit
2559-
log.debug("received sig for batch of size={}", commit.batchSize)
2559+
log.debug("received sig for batch of size={} for fundingTxId={}", commit.batchSize, commit.fundingTxId_opt)
25602560
if (sigStash.size == commit.batchSize) {
25612561
val sigs = sigStash
25622562
sigStash = Nil

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package fr.acinq.eclair.io
1818

1919
import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated}
2020
import akka.event.Logging.MDC
21-
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32}
21+
import fr.acinq.bitcoin.scalacompat.BlockHash
2222
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2323
import fr.acinq.eclair.Logs.LogCategory
2424
import fr.acinq.eclair.crypto.Noise.KeyPair
@@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
2828
import fr.acinq.eclair.router.Router._
2929
import fr.acinq.eclair.wire.protocol
3030
import fr.acinq.eclair.wire.protocol._
31-
import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureCompatibilityResult, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
31+
import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
3232
import scodec.Attempt
3333
import scodec.bits.ByteVector
3434

@@ -206,11 +206,20 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
206206
stay()
207207

208208
case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message
209-
d.transport forward msg
209+
val useExperimentalSplice = d.remoteInit.features.unknown.map(_.bitIndex).contains(155)
210+
msg match {
211+
// If our peer is using the experimental splice version, we convert splice messages.
212+
case msg: SpliceInit if useExperimentalSplice => d.transport forward ExperimentalSpliceInit.from(msg)
213+
case msg: SpliceAck if useExperimentalSplice => d.transport forward ExperimentalSpliceAck.from(msg)
214+
case msg: SpliceLocked if useExperimentalSplice => d.transport forward ExperimentalSpliceLocked.from(msg)
215+
case msg: TxAddInput if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxAddInputTlv.SharedInputTxId])))
216+
case msg: CommitSig if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.BatchTlv])))
217+
case msg: TxSignatures if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxSignaturesTlv.PreviousFundingTxSig])))
218+
case _ => d.transport forward msg
219+
}
210220
msg match {
211221
// If we send any channel management message to this peer, the connection should be persistent.
212-
case _: ChannelMessage if !d.isPersistent =>
213-
stay() using d.copy(isPersistent = true)
222+
case _: ChannelMessage if !d.isPersistent => stay() using d.copy(isPersistent = true)
214223
case _ => stay()
215224
}
216225

@@ -343,7 +352,13 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
343352
case Event(msg: LightningMessage, d: ConnectedData) =>
344353
// we acknowledge and pass all other messages to the peer
345354
d.transport ! TransportHandler.ReadAck(msg)
346-
d.peer ! msg
355+
msg match {
356+
// If our peer is using the experimental splice version, we convert splice messages.
357+
case msg: ExperimentalSpliceInit => d.peer ! msg.toSpliceInit()
358+
case msg: ExperimentalSpliceAck => d.peer ! msg.toSpliceAck()
359+
case msg: ExperimentalSpliceLocked => d.peer ! msg.toSpliceLocked()
360+
case _ => d.peer ! msg
361+
}
347362
stay()
348363

349364
case Event(readAck: TransportHandler.ReadAck, d: ConnectedData) =>

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package fr.acinq.eclair.wire.protocol
1818

1919
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
20+
import fr.acinq.bitcoin.scalacompat.TxId
2021
import fr.acinq.eclair.UInt64
2122
import fr.acinq.eclair.wire.protocol.CommonCodecs._
2223
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16}
@@ -73,16 +74,25 @@ object UpdateFailMalformedHtlcTlv {
7374
sealed trait CommitSigTlv extends Tlv
7475

7576
object CommitSigTlv {
77+
/**
78+
* While a splice is ongoing and not locked, we have multiple valid commitments.
79+
* We send one [[CommitSig]] message for each valid commitment.
80+
*
81+
* @param size the number of [[CommitSig]] messages in the batch.
82+
* @param fundingTxId the funding transaction spent by this commitment.
83+
*/
84+
case class BatchTlv(size: Int, fundingTxId: TxId) extends CommitSigTlv
7685

77-
/** @param size the number of [[CommitSig]] messages in the batch */
78-
case class BatchTlv(size: Int) extends CommitSigTlv
86+
private val batchTlv: Codec[BatchTlv] = tlvField(uint16 :: txIdAsHash)
7987

80-
object BatchTlv {
81-
val codec: Codec[BatchTlv] = tlvField(tu16)
82-
}
88+
/** Similar to [[BatchTlv]] for peers who only support the experimental version of splicing. */
89+
case class ExperimentalBatchTlv(size: Int) extends CommitSigTlv
90+
91+
private val experimentalBatchTlv: Codec[ExperimentalBatchTlv] = tlvField(tu16)
8392

8493
val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint)
85-
.typecase(UInt64(0x47010005), BatchTlv.codec)
94+
.typecase(UInt64(0), batchTlv)
95+
.typecase(UInt64(0x47010005), experimentalBatchTlv)
8696
)
8797

8898
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ object TxAddInputTlv {
3333
/** When doing a splice, the initiator must provide the previous funding txId instead of the whole transaction. */
3434
case class SharedInputTxId(txId: TxId) extends TxAddInputTlv
3535

36+
/** Same as [[SharedInputTxId]] for peers who only support the experimental version of splicing. */
37+
case class ExperimentalSharedInputTxId(txId: TxId) extends TxAddInputTlv
38+
3639
val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)
3740
// Note that we actually encode as a tx_hash to be consistent with other lightning messages.
38-
.typecase(UInt64(1105), tlvField(txIdAsHash.as[SharedInputTxId]))
41+
.typecase(UInt64(0), tlvField(txIdAsHash.as[SharedInputTxId]))
42+
.typecase(UInt64(1105), tlvField(txIdAsHash.as[ExperimentalSharedInputTxId]))
3943
)
4044
}
4145

@@ -69,8 +73,12 @@ object TxSignaturesTlv {
6973
/** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */
7074
case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv
7175

76+
/** Same as [[PreviousFundingTxSig]] for peers who only support the experimental version of splicing. */
77+
case class ExperimentalPreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv
78+
7279
val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)
73-
.typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig]))
80+
.typecase(UInt64(0), tlvField(bytes64.as[PreviousFundingTxSig]))
81+
.typecase(UInt64(601), tlvField(bytes64.as[ExperimentalPreviousFundingTxSig]))
7482
)
7583
}
7684

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,17 +410,36 @@ object LightningMessageCodecs {
410410
("fundingPubkey" | publicKey) ::
411411
("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[SpliceInit]
412412

413+
val experimentalSpliceInitCodec: Codec[ExperimentalSpliceInit] = (
414+
("channelId" | bytes32) ::
415+
("fundingContribution" | satoshiSigned) ::
416+
("feerate" | feeratePerKw) ::
417+
("lockTime" | uint32) ::
418+
("fundingPubkey" | publicKey) ::
419+
("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[ExperimentalSpliceInit]
420+
413421
val spliceAckCodec: Codec[SpliceAck] = (
414422
("channelId" | bytes32) ::
415423
("fundingContribution" | satoshiSigned) ::
416424
("fundingPubkey" | publicKey) ::
417425
("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[SpliceAck]
418426

427+
val experimentalSpliceAckCodec: Codec[ExperimentalSpliceAck] = (
428+
("channelId" | bytes32) ::
429+
("fundingContribution" | satoshiSigned) ::
430+
("fundingPubkey" | publicKey) ::
431+
("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[ExperimentalSpliceAck]
432+
419433
val spliceLockedCodec: Codec[SpliceLocked] = (
420434
("channelId" | bytes32) ::
421435
("fundingTxHash" | txIdAsHash) ::
422436
("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[SpliceLocked]
423437

438+
val experimentalSpliceLockedCodec: Codec[ExperimentalSpliceLocked] = (
439+
("channelId" | bytes32) ::
440+
("fundingTxHash" | txIdAsHash) ::
441+
("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[ExperimentalSpliceLocked]
442+
424443
val stfuCodec: Codec[Stfu] = (
425444
("channelId" | bytes32) ::
426445
("initiator" | byte.xmap[Boolean](b => b != 0, b => if (b) 1 else 0))).as[Stfu]
@@ -498,6 +517,9 @@ object LightningMessageCodecs {
498517
.typecase(72, txInitRbfCodec)
499518
.typecase(73, txAckRbfCodec)
500519
.typecase(74, txAbortCodec)
520+
.typecase(77, spliceLockedCodec)
521+
.typecase(80, spliceInitCodec)
522+
.typecase(81, spliceAckCodec)
501523
.typecase(128, updateAddHtlcCodec)
502524
.typecase(130, updateFulfillHtlcCodec)
503525
.typecase(131, updateFailHtlcCodec)
@@ -529,9 +551,9 @@ object LightningMessageCodecs {
529551
.typecase(41045, addFeeCreditCodec)
530552
.typecase(41046, currentFeeCreditCodec)
531553
//
532-
.typecase(37000, spliceInitCodec)
533-
.typecase(37002, spliceAckCodec)
534-
.typecase(37004, spliceLockedCodec)
554+
.typecase(37000, experimentalSpliceInitCodec)
555+
.typecase(37002, experimentalSpliceAckCodec)
556+
.typecase(37004, experimentalSpliceLockedCodec)
535557
//
536558

537559
//

0 commit comments

Comments
 (0)