Skip to content

Commit 6bef0dc

Browse files
authored
Use official splice messages (#768)
* Use official splice messages We replace our experimental version of `splice_init`, `splice_ack` and `splice_locked` by their official version. We also change the TLV fields added to `commit_sig`, `tx_add_input`, and `tx_signatures` to match the spec version. We only allow connecting to peers who support the official splicing feature. * Add `channel_reestablish` TLVs for retransmission With splicing, we introduce new TLVs to `channel_reestablish` to let our peer know: - the latest `splice_locked` (or `channel_ready`) we're ready to send or have sent before disconnecting - whether we need a retransmission of `commit_sig` for the next funding - whether we need a retransmission of `announcement_signatures` for the next funding (always false for mobile wallets) This lets us clean-up retransmission of those messages and follow the official splicing spec. * Update remote funding status on `channel_ready` When we receive the initial `channel_ready`, we update the remote funding status to be consistent with our reestablish behavior, where we would update it when receiving `my_current_funding_locked`.
1 parent 42d299a commit 6bef0dc

34 files changed

Lines changed: 457 additions & 293 deletions

File tree

modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ sealed class Feature {
161161
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
162162
}
163163

164+
@Serializable
165+
object Splicing : Feature() {
166+
override val rfcName get() = "option_splice"
167+
override val mandatory get() = 62
168+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
169+
}
170+
164171
// The following features have not been standardised, hence the high feature bits to avoid conflicts.
165172

166173
/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
@@ -196,13 +203,6 @@ sealed class Feature {
196203
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
197204
}
198205

199-
@Serializable
200-
object ExperimentalSplice : Feature() {
201-
override val rfcName get() = "splice_experimental"
202-
override val mandatory get() = 154
203-
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
204-
}
205-
206206
@Serializable
207207
object OnTheFlyFunding : Feature() {
208208
override val rfcName get() = "on_the_fly_funding"
@@ -295,11 +295,11 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
295295
Feature.ChannelType,
296296
Feature.PaymentMetadata,
297297
Feature.SimpleClose,
298+
Feature.Splicing,
298299
Feature.ExperimentalTrampolinePayment,
299300
Feature.ZeroReserveChannels,
300301
Feature.WakeUpNotificationClient,
301302
Feature.WakeUpNotificationProvider,
302-
Feature.ExperimentalSplice,
303303
Feature.OnTheFlyFunding,
304304
Feature.FundingFeeCredit,
305305
Feature.SimpleTaprootChannels
@@ -335,7 +335,6 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
335335
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
336336
Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit),
337337
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
338-
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
339338
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
340339
)
341340

modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ data class NodeParams(
202202
require(features.hasFeature(Feature.PaymentSecret, FeatureSupport.Mandatory)) { "${Feature.PaymentSecret.rfcName} should be mandatory" }
203203
require(features.hasFeature(Feature.ChannelType, FeatureSupport.Mandatory)) { "${Feature.ChannelType.rfcName} should be mandatory" }
204204
require(features.hasFeature(Feature.DualFunding, FeatureSupport.Mandatory)) { "${Feature.DualFunding.rfcName} should be mandatory" }
205+
require(features.hasFeature(Feature.Splicing, FeatureSupport.Mandatory)) { "${Feature.Splicing.rfcName} should be mandatory" }
205206
require(features.hasFeature(Feature.RouteBlinding)) { "${Feature.RouteBlinding.rfcName} should be supported" }
206207
require(features.hasFeature(Feature.ShutdownAnySegwit, FeatureSupport.Mandatory)) { "${Feature.ShutdownAnySegwit.rfcName} should be mandatory" }
207208
require(features.hasFeature(Feature.SimpleClose, FeatureSupport.Mandatory)) { "${Feature.SimpleClose.rfcName} should be mandatory" }
@@ -233,10 +234,10 @@ data class NodeParams(
233234
Feature.ChannelType to FeatureSupport.Mandatory,
234235
Feature.PaymentMetadata to FeatureSupport.Optional,
235236
Feature.SimpleClose to FeatureSupport.Mandatory,
237+
Feature.Splicing to FeatureSupport.Mandatory,
236238
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
237239
Feature.ZeroReserveChannels to FeatureSupport.Optional,
238240
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
239-
Feature.ExperimentalSplice to FeatureSupport.Optional,
240241
Feature.OnTheFlyFunding to FeatureSupport.Optional,
241242
Feature.FundingFeeCredit to FeatureSupport.Optional,
242243
),

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
167167
remoteFundingPubKey: PublicKey,
168168
commitInput: Transactions.InputInfo,
169169
commitmentFormat: Transactions.CommitmentFormat,
170-
batchSize: Int,
171170
remoteNonce: IndividualNonce?,
172171
logger: MDCLogger
173172
): Either<ChannelException, CommitSig> {
@@ -194,15 +193,15 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
194193
return when (commitmentFormat) {
195194
Transactions.CommitmentFormat.AnchorOutputs -> {
196195
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
197-
Either.Right(CommitSig(channelParams.channelId, sig, htlcSigs, batchSize))
196+
Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, sig, htlcSigs))
198197
}
199198
Transactions.CommitmentFormat.SimpleTaprootChannels -> when (remoteNonce) {
200199
null -> Either.Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
201200
else -> {
202201
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubKey, commitInput.outPoint.txid)
203202
when (val psig = remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce))) {
204203
is Either.Left -> Either.Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
205-
is Either.Right -> Either.Right(CommitSig(channelParams.channelId, psig.value, htlcSigs, batchSize))
204+
is Either.Right -> Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, psig.value, htlcSigs))
206205
}
207206
}
208207
}
@@ -218,7 +217,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
218217
signingSession.fundingParams.remoteFundingPubkey,
219218
signingSession.commitInput(channelKeys),
220219
signingSession.fundingParams.commitmentFormat,
221-
batchSize = 1,
222220
remoteNonce,
223221
logger
224222
)
@@ -580,7 +578,6 @@ data class Commitment(
580578
commitKeys: RemoteCommitmentKeys,
581579
changes: CommitmentChanges,
582580
remoteNextPerCommitmentPoint: PublicKey,
583-
batchSize: Int,
584581
nextRemoteNonce: IndividualNonce?,
585582
logger: MDCLogger
586583
): Either<ChannelException, Pair<Commitment, CommitSig>> {
@@ -618,7 +615,7 @@ data class Commitment(
618615
val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",")
619616
"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId"
620617
}
621-
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), batchSize)
618+
val commitSig = CommitSig(params.channelId, fundingTxId, sig, htlcSigs.toList())
622619
val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
623620
return Either.Right(Pair(commitment1, commitSig))
624621
}
@@ -710,6 +707,8 @@ data class Commitments(
710707
// We always use the last commitment that was created, to make sure we never go back in time.
711708
val latest = FullCommitment(channelParams, changes, active.first())
712709

710+
fun lastLocalLocked(zeroConf: Boolean): Commitment? = active.find { zeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
711+
713712
val all = buildList {
714713
addAll(active)
715714
addAll(inactive)
@@ -885,7 +884,7 @@ data class Commitments(
885884
val (active1, sigs) = active.map { c ->
886885
val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint)
887886
val remoteNonce = remoteCommitNonces[c.fundingTxId]
888-
when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, remoteNonce, logger)) {
887+
when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, remoteNonce, logger)) {
889888
is Either.Left -> return Either.Left(res.left)
890889
is Either.Right -> res.value
891890
}
@@ -913,9 +912,12 @@ data class Commitments(
913912
return Either.Left(CommitSigCountMismatch(channelId, active.size, sigs.size))
914913
}
915914
val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex + 1)
916-
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
917-
val active1 = active.zip(sigs).map {
918-
when (val commitment1 = it.first.receiveCommit(channelParams, channelKeys, commitKeys, changes, it.second, logger)) {
915+
val active1 = active.withIndex().map { (i, c) ->
916+
// If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first).
917+
// This ensures that the case where we have a batch of a single element (no pending splice) works correctly.
918+
// This also ensures that we get a signature failure when our set of funding txs doesn't match with our peer.
919+
val commit = sigs.find { it.fundingTxId == c.fundingTxId } ?: sigs[i]
920+
when (val commitment1 = c.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit, logger)) {
919921
is Either.Left -> return Either.Left(commitment1.value)
920922
is Either.Right -> commitment1.value
921923
}
@@ -1084,7 +1086,7 @@ data class Commitments(
10841086
// This ensures that we only have to send splice_locked for the latest commitment instead of sending it for every commitment.
10851087
// A side-effect is that previous commitments that are implicitly locked don't necessarily have their status correctly set.
10861088
// That's why we look at locked commitments separately and then select the one with the oldest fundingTxIndex.
1087-
val lastLocalLocked = active.find { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
1089+
val lastLocal = lastLocalLocked(staticParams.useZeroConf)
10881090
val lastRemoteLocked = active.find { it.remoteFundingStatus == RemoteFundingStatus.Locked }
10891091
return when {
10901092
// We select the locked commitment with the smaller value for fundingTxIndex, but both have to be defined.
@@ -1093,9 +1095,9 @@ data class Commitments(
10931095
// - transactions with the same fundingTxIndex double-spend each other, so only one of them can confirm
10941096
// - we don't allow creating a splice on top of an unconfirmed transaction that has RBF attempts (because it
10951097
// would become invalid if another of the RBF attempts end up being confirmed)
1096-
lastLocalLocked != null && lastRemoteLocked != null -> listOf(lastLocalLocked, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
1098+
lastLocal != null && lastRemoteLocked != null -> listOf(lastLocal, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
10971099
// Special case for the initial funding tx, we only require a local lock because channel_ready doesn't explicitly reference a funding tx.
1098-
lastLocalLocked != null && lastLocalLocked.fundingTxIndex == 0L -> lastLocalLocked
1100+
lastLocal != null && lastLocal.fundingTxIndex == 0L -> lastLocal
10991101
else -> null
11001102
}
11011103
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ data class InteractiveTxSession(
731731
fundingParams,
732732
localCommitIndex,
733733
SharedFundingInputBalances(previousLocalBalance, previousRemoteBalance, localHtlcs.map { it.add.amountMsat }.sum()),
734-
fundingContributions.inputs.map { i -> Either.Left<InteractiveTxInput.Outgoing>(i) } + fundingContributions.outputs.map { o -> Either.Right<InteractiveTxOutput.Outgoing>(o) },
734+
fundingContributions.inputs.map { i -> Either.Left(i) } + fundingContributions.outputs.map { o -> Either.Right(o) },
735735
previousTxs,
736736
localHtlcs,
737737
localFundingNonce = fundingParams.sharedInput?.let {
@@ -1106,11 +1106,8 @@ data class InteractiveTxSigningSession(
11061106
// +-------+ +-------+
11071107
val fundingTxId: TxId = fundingTx.txId
11081108
val localCommitIndex = localCommit.fold({ it.index }, { it.index })
1109-
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
1110-
val nextLocalCommitmentNumber = when (localCommit) {
1111-
is Either.Left -> localCommit.value.index
1112-
is Either.Right -> localCommit.value.index + 1
1113-
}
1109+
// If we haven't received the remote commit_sig, we will request a retransmission on reconnection.
1110+
val retransmitRemoteCommitSig: Boolean = localCommit.isLeft
11141111

11151112
fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = fundingParams.fundingKey(channelKeys)
11161113

@@ -1273,7 +1270,7 @@ data class InteractiveTxSigningSession(
12731270
val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",")
12741271
"built remote commit number=$remoteCommitmentIndex toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${firstCommitTx.remoteCommitTx.tx.txid} fundingTxId=${unsignedTx.txid}"
12751272
}
1276-
val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, batchSize = 1)
1273+
val commitSig = CommitSig(channelParams.channelId, unsignedTx.txid, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs)
12771274
// We haven't received the remote commit_sig: we don't have local htlc txs yet.
12781275
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx.tx.txid)
12791276
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -317,37 +317,27 @@ sealed class PersistedChannelState : ChannelState() {
317317
}
318318
ChannelReestablish(
319319
channelId = channelId,
320-
nextLocalCommitmentNumber = state.signingSession.nextLocalCommitmentNumber,
320+
nextLocalCommitmentNumber = 1,
321321
nextRemoteRevocationNumber = 0,
322322
yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
323323
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
324324
nextCommitNonces = nextCommitNonce?.let { listOf(nextFundingTxId to it) } ?: listOf(),
325325
nextFundingTxId = nextFundingTxId,
326+
retransmitCommitSig = state.signingSession.retransmitRemoteCommitSig,
326327
currentCommitNonce = currentCommitNonce
327328
)
328329
}
329330
is ChannelStateWithCommitments -> {
330331
val channelKeys = channelKeys()
331332
val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes
332333
val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(state.commitments.localCommitIndex)
333-
// If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig.
334-
val nextLocalCommitmentNumber = when (state) {
335-
is WaitForFundingConfirmed -> when (state.rbfStatus) {
336-
is RbfStatus.WaitingForSigs -> state.rbfStatus.session.nextLocalCommitmentNumber
337-
else -> state.commitments.localCommitIndex + 1
338-
}
339-
is Normal -> when (state.spliceStatus) {
340-
is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.nextLocalCommitmentNumber
341-
else -> state.commitments.localCommitIndex + 1
342-
}
343-
else -> state.commitments.localCommitIndex + 1
344-
}
345-
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures.
346-
val unsignedFundingTxId = when (state) {
334+
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures and commit_sig.
335+
val (unsignedFundingTxId, retransmitCommitSig) = when (state) {
347336
is WaitForFundingConfirmed -> state.getUnsignedFundingTxId()
348337
is Normal -> state.getUnsignedFundingTxId()
349-
else -> null
338+
else -> Pair(null, false)
350339
}
340+
val lastFundingLocked = state.commitments.lastLocalLocked(staticParams.useZeroConf)
351341
// We send our verification nonces for all active commitments.
352342
val nextCommitNonces = state.commitments.active.mapNotNull { c ->
353343
when (c.commitmentFormat) {
@@ -378,12 +368,14 @@ sealed class PersistedChannelState : ChannelState() {
378368
}
379369
ChannelReestablish(
380370
channelId = channelId,
381-
nextLocalCommitmentNumber = nextLocalCommitmentNumber,
371+
nextLocalCommitmentNumber = state.commitments.localCommitIndex + 1,
382372
nextRemoteRevocationNumber = state.commitments.remoteCommitIndex,
383373
yourLastCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
384374
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
385375
nextCommitNonces = nextCommitNonces + listOfNotNull(interactiveTxNextCommitNonce),
386376
nextFundingTxId = unsignedFundingTxId,
377+
retransmitCommitSig = retransmitCommitSig,
378+
currentFundingLocked = lastFundingLocked?.fundingTxId,
387379
currentCommitNonce = interactiveTxCurrentCommitNonce,
388380
)
389381
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -935,11 +935,14 @@ data class Normal(
935935
return Pair(nextState, actions)
936936
}
937937

938-
/** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */
939-
fun getUnsignedFundingTxId(): TxId? = when {
940-
spliceStatus is SpliceStatus.WaitingForSigs -> spliceStatus.session.fundingTx.txId
941-
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> commitments.latest.localFundingStatus.txId
942-
else -> null
938+
/**
939+
* If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction.
940+
* The second parameter should be set to true when commit_sig also needs to be retransmitted.
941+
*/
942+
fun getUnsignedFundingTxId(): Pair<TxId?, Boolean> = when {
943+
spliceStatus is SpliceStatus.WaitingForSigs -> Pair(spliceStatus.session.fundingTx.txId, spliceStatus.session.retransmitRemoteCommitSig)
944+
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> Pair(commitments.latest.localFundingStatus.txId, false)
945+
else -> Pair(null, false)
943946
}
944947

945948
/** Returns true if the [shortChannelId] matches one of our commitments or our alias. */

0 commit comments

Comments
 (0)