Skip to content

Commit 4a306f1

Browse files
committed
Implement simple taproot channels
This matches changes done on Eclair, and adds support for taproot channels (including splices) with the same TLV extensions. Support for signing commit tx with alternative feerates is not implemented.
1 parent 7d18500 commit 4a306f1

35 files changed

+1285
-290
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ sealed class Feature {
217217
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
218218
}
219219

220+
// README: this is not the feature bit specified in the BOLT, this one is specific to Phoenix
221+
@Serializable
222+
object SimpleTaprootChannels : Feature() {
223+
override val rfcName get() = "simple_taproot_channels"
224+
override val mandatory get() = 564
225+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
226+
}
220227
}
221228

222229
@Serializable
@@ -294,7 +301,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
294301
Feature.WakeUpNotificationProvider,
295302
Feature.ExperimentalSplice,
296303
Feature.OnTheFlyFunding,
297-
Feature.FundingFeeCredit
304+
Feature.FundingFeeCredit,
305+
Feature.SimpleTaprootChannels
298306
)
299307

300308
operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,7 @@ data class PleasePublishYourCommitment (override val channelId: Byte
9292
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
9393
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")
9494
data class InvalidSpliceRequest (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice request")
95+
data class MissingCommitNonce (override val channelId: ByteVector32, val fundingTxId: TxId, val commitmentNumber: Long) : ChannelException(channelId, "missing commit nonce for funding tx $fundingTxId commitmentNumber $commitmentNumber")
96+
data class InvalidCommitNonce (override val channelId: ByteVector32, val fundingTxId: TxId, val commitmentNumber: Long) : ChannelException(channelId, "invalid commit nonce for funding tx $fundingTxId commitmentNumber $commitmentNumber")
97+
data class MissingClosingNonce (override val channelId: ByteVector32) : ChannelException(channelId, "missing closing nonce")
9598
// @formatter:on

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ data class ChannelFeatures(val features: Set<Feature>) {
3232
* In addition to channel types features, the following features will be added to the permanent channel features if they
3333
* are supported by both peers.
3434
*/
35-
private val permanentChannelFeatures = setOf(Feature.DualFunding)
35+
private val permanentChannelFeatures: Set<Feature> = setOf(Feature.DualFunding, Feature.SimpleTaprootChannels)
3636
}
3737

3838
}
@@ -65,6 +65,12 @@ sealed class ChannelType {
6565
override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.AnchorOutputs
6666
}
6767

68+
object SimpleTaprootChannels : SupportedChannelType() {
69+
override val name: String get() = "simple_taproot_channel"
70+
override val features: Set<Feature> get() = setOf(Feature.SimpleTaprootChannels, Feature.ZeroReserveChannels)
71+
override val permanentChannelFeatures: Set<Feature> get() = setOf()
72+
override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.SimpleTaprootChannels
73+
}
6874
}
6975

7076
data class UnsupportedChannelType(val featureBits: Features) : ChannelType() {
@@ -79,6 +85,7 @@ sealed class ChannelType {
7985
// NB: Bolt 2: features must exactly match in order to identify a channel type.
8086
fun fromFeatures(features: Features): ChannelType = when (features) {
8187
// @formatter:off
88+
Features(Feature.SimpleTaprootChannels to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.SimpleTaprootChannels
8289
Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputsZeroReserve
8390
Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputs
8491
else -> UnsupportedChannelType(features)

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

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import fr.acinq.lightning.channel.states.ChannelContext
1616
import fr.acinq.lightning.crypto.ChannelKeys
1717
import fr.acinq.lightning.crypto.KeyManager
1818
import fr.acinq.lightning.crypto.LocalCommitmentKeys
19+
import fr.acinq.lightning.crypto.NonceGenerator
1920
import fr.acinq.lightning.crypto.RemoteCommitmentKeys
2021
import fr.acinq.lightning.crypto.ShaChain
2122
import fr.acinq.lightning.logging.MDCLogger
@@ -34,6 +35,7 @@ import fr.acinq.lightning.transactions.incomings
3435
import fr.acinq.lightning.transactions.outgoings
3536
import fr.acinq.lightning.utils.*
3637
import fr.acinq.lightning.wire.*
38+
import kotlinx.serialization.Transient
3739
import kotlin.math.min
3840

3941
/** Static channel parameters shared by all commitments. */
@@ -63,7 +65,7 @@ data class RemoteChanges(val proposed: List<UpdateMessage>, val acked: List<Upda
6365
val all: List<UpdateMessage> get() = proposed + signed + acked
6466
}
6567

66-
/** Changes are applied to all commitments, and must be be valid for all commitments. */
68+
/** Changes are applied to all commitments, and must be valid for all commitments. */
6769
data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges: RemoteChanges, val localNextHtlcId: Long, val remoteNextHtlcId: Long) {
6870
fun addLocalProposal(proposal: UpdateMessage): CommitmentChanges = copy(localChanges = localChanges.copy(proposed = localChanges.proposed + proposal))
6971

@@ -130,7 +132,20 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId
130132
commitmentFormat = commitmentFormat,
131133
spec = spec,
132134
)
133-
if (!localCommitTx.checkRemoteSig(fundingKey.publicKey(), remoteFundingPubKey, commit.signature)) {
135+
val remoteSigOk = when (commitmentFormat) {
136+
Transactions.CommitmentFormat.SimpleTaprootChannels ->
137+
when (commit.sigOrPartialSig) {
138+
is ChannelSpendSignature.PartialSignatureWithNonce -> {
139+
val localNonce = NonceGenerator.verificationNonce(commitInput.outPoint.txid, fundingKey, remoteFundingPubKey, localCommitIndex)
140+
localCommitTx.checkRemotePartialSignature(fundingKey.publicKey(), remoteFundingPubKey, commit.sigOrPartialSig, localNonce.publicNonce)
141+
}
142+
143+
is ChannelSpendSignature.IndividualSignature -> false
144+
}
145+
146+
else -> localCommitTx.checkRemoteSig(fundingKey.publicKey(), remoteFundingPubKey, commit.signature)
147+
}
148+
if (!remoteSigOk) {
134149
log.error { "remote signature $commit is invalid" }
135150
return Either.Left(InvalidCommitmentSignature(channelParams.channelId, localCommitTx.tx.txid))
136151
}
@@ -142,7 +157,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId
142157
return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid))
143158
}
144159
}
145-
return Either.Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.signature, commit.htlcSignatures))
160+
return Either.Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.sigOrPartialSig, commit.htlcSignatures))
146161
}
147162
}
148163
}
@@ -157,6 +172,7 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
157172
remoteFundingPubKey: PublicKey,
158173
commitInput: Transactions.InputInfo,
159174
commitmentFormat: Transactions.CommitmentFormat,
175+
remoteNonce: IndividualNonce?,
160176
batchSize: Int = 1
161177
): CommitSig {
162178
val fundingKey = channelKeys.fundingKey(fundingTxIndex)
@@ -172,23 +188,28 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
172188
commitmentFormat = commitmentFormat,
173189
spec = spec
174190
)
175-
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
176-
val htlcSigs = sortedHtlcsTxs.map { it.localSig(commitKeys) }
177-
val tlvs = buildSet<CommitSigTlv> {
178-
if (batchSize > 1) add(CommitSigTlv.Batch(batchSize))
191+
val sig = when (commitmentFormat) {
192+
is Transactions.CommitmentFormat.SimpleTaprootChannels -> {
193+
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubKey, commitInput.outPoint.txid)
194+
remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce!!)).right!! // FIXME
195+
}
196+
197+
else -> remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
179198
}
180-
return CommitSig(channelParams.channelId, sig, htlcSigs.toList(), TlvStream(tlvs))
199+
val htlcSigs = sortedHtlcsTxs.map { it.localSig(commitKeys) }
200+
return CommitSig(channelParams.channelId, sig, htlcSigs.toList(), batchSize)
181201
}
182202

183-
fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession): CommitSig {
203+
fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession, remoteNonce: IndividualNonce?): CommitSig {
184204
return sign(
185205
channelParams,
186206
signingSession.remoteCommitParams,
187207
channelKeys,
188208
signingSession.fundingTxIndex,
189209
signingSession.fundingParams.remoteFundingPubkey,
190210
signingSession.commitInput(channelKeys),
191-
signingSession.fundingParams.commitmentFormat
211+
signingSession.fundingParams.commitmentFormat,
212+
remoteNonce
192213
)
193214
}
194215
}
@@ -255,7 +276,14 @@ data class Commitment(
255276
val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubkey)
256277
unsignedCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubkey, localSig, remoteSig)
257278
}
258-
else -> throw IllegalArgumentException("not implemented") // FIXME
279+
is ChannelSpendSignature.PartialSignatureWithNonce -> {
280+
val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubkey, localCommit.index)
281+
// We have already validated the remote nonce and partial signature when we received it, so we're guaranteed
282+
// that the following code cannot produce an error.
283+
val localSig = unsignedCommitTx.partialSign(fundingKey, remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteSig.nonce)).right!!
284+
val signedTx = unsignedCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubkey, localSig, remoteSig, mapOf()).right!!
285+
signedTx
286+
}
259287
}
260288
}
261289

@@ -518,7 +546,16 @@ data class Commitment(
518546
}
519547
}
520548

521-
fun sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair<Commitment, CommitSig> {
549+
fun sendCommit(
550+
params: ChannelParams,
551+
channelKeys: ChannelKeys,
552+
commitKeys: RemoteCommitmentKeys,
553+
changes: CommitmentChanges,
554+
remoteNextPerCommitmentPoint: PublicKey,
555+
batchSize: Int,
556+
nextRemoteNonce: IndividualNonce?,
557+
log: MDCLogger
558+
): Pair<Commitment, CommitSig> {
522559
val fundingKey = localFundingKey(channelKeys)
523560
// remote commitment will include all local changes + remote acked changes
524561
val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed)
@@ -533,7 +570,18 @@ data class Commitment(
533570
commitmentFormat = commitmentFormat,
534571
spec = spec
535572
)
536-
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubkey)
573+
val sig = when (commitmentFormat) {
574+
Transactions.CommitmentFormat.SimpleTaprootChannels -> ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)
575+
else -> remoteCommitTx.sign(fundingKey, remoteFundingPubkey)
576+
}
577+
val partialSig = when (commitmentFormat) {
578+
Transactions.CommitmentFormat.SimpleTaprootChannels -> {
579+
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubkey, remoteCommitTx.input.outPoint.txid)
580+
remoteCommitTx.partialSign(fundingKey, remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, nextRemoteNonce!!)).right!!
581+
}
582+
583+
else -> null
584+
}
537585
val htlcSigs = sortedHtlcTxs.map { it.localSig(commitKeys) }
538586

539587
// NB: IN/OUT htlcs are inverted because this is the remote commit
@@ -566,6 +614,7 @@ data class Commitment(
566614
if (batchSize > 1) {
567615
add(CommitSigTlv.Batch(batchSize))
568616
}
617+
partialSig?.let { add(CommitSigTlv.PartialSignatureWithNonce(it)) }
569618
}
570619
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs))
571620
val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
@@ -644,6 +693,10 @@ data class Commitments(
644693
val payments: Map<Long, UUID>, // for outgoing htlcs, maps to paymentId
645694
val remoteNextCommitInfo: Either<WaitingForRevocation, PublicKey>, // this one is tricky, it must be kept in sync with Commitment.nextRemoteCommit
646695
val remotePerCommitmentSecrets: ShaChain,
696+
@Transient val remoteCommitNonces: Map<TxId, IndividualNonce>,
697+
@Transient val localCloseeNonce: Transactions.LocalNonce?,
698+
@Transient val remoteCloseeNonce: IndividualNonce?,
699+
@Transient val localCloserNonces: Transactions.CloserNonces?,
647700
) {
648701
init {
649702
require(active.isNotEmpty()) { "there must be at least one active commitment" }
@@ -674,6 +727,10 @@ data class Commitments(
674727
addAll(active)
675728
})
676729

730+
fun addRemoteCommitNonce(fundingTxId: TxId, nonce: IndividualNonce?): Commitments = nonce?.let { this.copy(remoteCommitNonces = this.remoteCommitNonces + (fundingTxId to it)) } ?: this
731+
732+
fun resetNonces(): Commitments = copy(remoteCommitNonces = emptyMap(), localCloseeNonce = null, remoteCloseeNonce = null, localCloserNonces = null)
733+
677734
fun channelKeys(keyManager: KeyManager): ChannelKeys = channelParams.localParams.channelKeys(keyManager)
678735

679736
fun isMoreRecent(other: Commitments): Boolean {
@@ -838,7 +895,7 @@ data class Commitments(
838895
val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId))
839896
val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint)
840897
if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId))
841-
val (active1, sigs) = active.map { it.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip()
898+
val (active1, sigs) = active.map { it.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, remoteCommitNonces.get(it.fundingTxId), log) }.unzip()
842899
val commitments1 = copy(
843900
active = active1,
844901
remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)),
@@ -868,7 +925,12 @@ data class Commitments(
868925
// we will send our revocation preimage + our next revocation hash
869926
val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex)
870927
val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2)
871-
val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint)
928+
val localCommitNonces = active.filter { it.commitmentFormat == Transactions.CommitmentFormat.SimpleTaprootChannels }.map {
929+
val localNonce = NonceGenerator.verificationNonce(it.fundingTxId, it.localFundingKey(channelKeys), it.remoteFundingPubkey, localCommitIndex + 2)
930+
it.fundingTxId to localNonce.publicNonce
931+
}
932+
val tlvs: Set<RevokeAndAckTlv> = if (localCommitNonces.isEmpty()) setOf() else setOf(RevokeAndAckTlv.NextLocalNonces(localCommitNonces))
933+
val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint, TlvStream(tlvs))
872934
val commitments1 = copy(
873935
active = active1,
874936
changes = changes.copy(
@@ -929,10 +991,22 @@ data class Commitments(
929991
remoteNextCommitInfo = Either.Right(revocation.nextPerCommitmentPoint),
930992
remotePerCommitmentSecrets = remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret.value, 0xFFFFFFFFFFFFL - remoteCommitIndex),
931993
payments = payments1,
994+
remoteCommitNonces = revocation.nextCommitNonces
932995
)
933996
return Either.Right(Pair(commitments1, actions.toList()))
934997
}
935998

999+
fun createShutdown(channelKeys: ChannelKeys, finalScriptPubKey: ByteVector): Pair<Commitments, Shutdown> = when (latest.commitmentFormat) {
1000+
is Transactions.CommitmentFormat.SimpleTaprootChannels -> {
1001+
// We create a fresh local closee nonce every time we send shutdown.
1002+
val localFundingPubKey = channelKeys.fundingKey(latest.fundingTxIndex).publicKey()
1003+
val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, latest.remoteFundingPubkey, latest.fundingTxId)
1004+
this.copy(localCloseeNonce = localCloseeNonce) to Shutdown(channelId, finalScriptPubKey, TlvStream<ShutdownTlv>(ShutdownTlv.ShutdownNonce(localCloseeNonce.publicNonce)))
1005+
}
1006+
1007+
else -> this to Shutdown(channelId, finalScriptPubKey)
1008+
}
1009+
9361010
private fun ChannelContext.updateFundingStatus(fundingTxId: TxId, updateMethod: (Commitment, Long) -> Commitment): Either<Commitments, Pair<Commitments, Commitment>> {
9371011
return when (val c = all.find { it.fundingTxId == fundingTxId }) {
9381012
is Commitment -> {

0 commit comments

Comments
 (0)