Skip to content

Commit

Permalink
Send partial signature for alternate remote commit txs
Browse files Browse the repository at this point in the history
For taproot channels, when we send additional commit signatures for different feerate, we send a partial signature instead of a standard signature.
  • Loading branch information
sstone committed Nov 28, 2024
1 parent 89fbe38 commit 76e23a4
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 191 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -541,13 +541,41 @@ data class Commitment(

val tlvs = buildSet {
if (spec.htlcs.isEmpty()) {
val alternativeSigs = Commitments.alternativeFeerates.map { feerate ->
val alternativeRemoteCommitTxs = Commitments.alternativeFeerates.map { feerate ->
val alternativeSpec = spec.copy(feerate = feerate)
val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(channelKeys, commitTxNumber = remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex = fundingTxIndex, remoteFundingPubKey = remoteFundingPubkey, commitInput, remotePerCommitmentPoint = remoteNextPerCommitmentPoint, alternativeSpec)
val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex))
CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig)
val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(
channelKeys,
commitTxNumber = remoteCommit.index + 1,
params.localParams,
params.remoteParams,
fundingTxIndex = fundingTxIndex,
remoteFundingPubKey = remoteFundingPubkey,
commitInput,
remotePerCommitmentPoint = remoteNextPerCommitmentPoint,
alternativeSpec
)
feerate to alternativeRemoteCommitTx
}
when (remoteNonce) {
null -> {
val alternativeSigs = alternativeRemoteCommitTxs.map {
val alternativeSig = Transactions.sign(it.second, channelKeys.fundingKey(fundingTxIndex))
CommitSigTlv.AlternativeFeerateSig(it.first, alternativeSig)
}
add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs))
}

else -> {
val alternativeSigs = alternativeRemoteCommitTxs.map {
val localNonce = channelKeys.signingNonce(fundingTxIndex)
val fundingKey = channelKeys.fundingKey(fundingTxIndex)
val alternativePsig = Transactions.partialSign(it.second, fundingKey, fundingKey.publicKey(), remoteFundingPubkey, localNonce, remoteNonce).right!!
log.debug { "sendCommit: creating partial sig $alternativePsig for alternative remote commit tx ${it.second.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint" }
CommitSigTlv.AlternativeFeeratePartialSig(it.first, PartialSignatureWithNonce(alternativePsig, localNonce.second))
}
add(CommitSigTlv.AlternativeFeeratePartialSigs(alternativeSigs))
}
}
add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs))
}
if (batchSize > 1) {
add(CommitSigTlv.Batch(batchSize))
Expand All @@ -556,6 +584,7 @@ data class Commitment(
add(CommitSigTlv.PartialSignatureWithNonceTlv(partialSig))
}
}

val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs))
val commitment1 = copy(nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)))
return Pair(commitment1, commitSig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1240,26 +1240,41 @@ data class InteractiveTxSigningSession(
}
val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { it.sign(channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) }

val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) {
val commitSigTlvs = Commitments.alternativeFeerates.map { feerate ->
val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate)
val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(
channelKeys,
remoteCommitmentIndex,
channelParams.localParams,
channelParams.remoteParams,
fundingTxIndex,
fundingParams.remoteFundingPubkey,
firstCommitTx.remoteCommitTx.input,
remotePerCommitmentPoint,
alternativeSpec
)
val sig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex))
CommitSigTlv.AlternativeFeerateSig(feerate, sig)
val alternativeSigs = when {
firstCommitTx.remoteHtlcTxs.isNotEmpty() -> null
else -> {
val alts = Commitments.alternativeFeerates.map { feerate ->
val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate)
val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(
channelKeys,
remoteCommitmentIndex,
channelParams.localParams,
channelParams.remoteParams,
fundingTxIndex,
fundingParams.remoteFundingPubkey,
firstCommitTx.remoteCommitTx.input,
remotePerCommitmentPoint,
alternativeSpec
)
feerate to alternativeRemoteCommitTx
}
when (session.firstRemoteNonce) {
null -> CommitSigTlv.AlternativeFeerateSigs(alts.map {
val sig = Transactions.sign(it.second, channelKeys.fundingKey(fundingTxIndex))
CommitSigTlv.AlternativeFeerateSig(it.first, sig)
})

else -> CommitSigTlv.AlternativeFeeratePartialSigs(alts.map {
val localNonce = channelKeys.signingNonce(fundingTxIndex)
val psig = Transactions.partialSign(
it.second, channelKeys.fundingKey(fundingTxIndex),
channelKeys.fundingKey(fundingTxIndex).publicKey(), session.fundingParams.remoteFundingPubkey,
localNonce, session.firstRemoteNonce
).right!!
CommitSigTlv.AlternativeFeeratePartialSig(it.first, PartialSignatureWithNonce(psig, localNonce.second))
})
}
}
CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs)
} else {
null
}
val tlvStream = TlvStream(setOf(localPartialSigOfRemoteTx, alternativeSigs).filterNotNull().toSet())
val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, tlvStream)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,21 +345,21 @@ sealed class PersistedChannelState : ChannelState() {
)
}

state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx -> {
else -> {
logger.info { "splice may not have confirmed yet, re-sending splice nonces" }
listOf(
channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex).second,
channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex + 1).second
)
}
else -> null
}
val unsignedFundingTxId = when (state) {
is WaitForFundingConfirmed -> state.getUnsignedFundingTxId()
is Normal -> state.getUnsignedFundingTxId() // a splice was in progress, we tell our peer that we are remembering it and are expecting signatures
else -> null
}
val tlvs: TlvStream<ChannelReestablishTlv> = TlvStream(setOfNotNull(
val tlvs: TlvStream<ChannelReestablishTlv> = TlvStream(
setOfNotNull(
unsignedFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) },
myNextLocalNonces?.let { ChannelReestablishTlv.NextLocalNoncesTlv(it) },
spliceNonces?.let { ChannelReestablishTlv.SpliceNoncesTlv(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
JsonSerializers.ChannelReadyTlvSerializer::class,
JsonSerializers.CommitSigTlvAlternativeFeerateSigSerializer::class,
JsonSerializers.CommitSigTlvAlternativeFeerateSigsSerializer::class,
JsonSerializers.CommitSigTlvAlternativeFeeratePartialSigSerializer::class,
JsonSerializers.CommitSigTlvAlternativeFeeratePartialSigsSerializer::class,
JsonSerializers.CommitSigTlvBatchSerializer::class,
JsonSerializers.CommitSigTlvPartialSignatureWithNonceSerializer::class,
JsonSerializers.CommitSigTlvSerializer::class,
Expand Down Expand Up @@ -214,6 +216,7 @@ object JsonSerializers {
subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer)
subclass(ChannelReadyTlv.NextLocalNonceTlv::class, ChannelReadyTlvNextLocalNonceTlvSerializer)
subclass(CommitSigTlv.AlternativeFeerateSigs::class, CommitSigTlvAlternativeFeerateSigsSerializer)
subclass(CommitSigTlv.AlternativeFeeratePartialSigs::class, CommitSigTlvAlternativeFeeratePartialSigsSerializer)
subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer)
subclass(CommitSigTlv.PartialSignatureWithNonceTlv::class, CommitSigTlvPartialSignatureWithNonceSerializer)
subclass(ShutdownTlv.ChannelData::class, ShutdownTlvChannelDataSerializer)
Expand Down Expand Up @@ -565,6 +568,12 @@ object JsonSerializers {
@Serializer(forClass = CommitSigTlv.AlternativeFeerateSigs::class)
object CommitSigTlvAlternativeFeerateSigsSerializer

@Serializer(forClass = CommitSigTlv.AlternativeFeeratePartialSig::class)
object CommitSigTlvAlternativeFeeratePartialSigSerializer

@Serializer(forClass = CommitSigTlv.AlternativeFeeratePartialSigs::class)
object CommitSigTlvAlternativeFeeratePartialSigsSerializer

@Serializer(forClass = CommitSigTlv.Batch::class)
object CommitSigTlvBatchSerializer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.ChannelType
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.channel.Origin
import fr.acinq.lightning.channel.PartialSignatureWithNonce
import fr.acinq.lightning.wire.ChannelReadyTlv.NextLocalNonceTlv

sealed class ChannelTlv : Tlv {
/** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */
Expand Down Expand Up @@ -217,6 +215,42 @@ sealed class CommitSigTlv : Tlv {
}
}
}

data class AlternativeFeeratePartialSig(val feerate: FeeratePerKw, val psig: PartialSignatureWithNonce)

/**
* When there are no pending HTLCs, we provide a list of signatures for the commitment transaction signed at various feerates.
* This gives more options to the remote node to recover their funds if the user disappears without closing channels.
*/
data class AlternativeFeeratePartialSigs(val psigs: List<AlternativeFeeratePartialSig>) : CommitSigTlv() {
override val tag: Long get() = AlternativeFeeratePartialSigs.tag
override fun write(out: Output) {
LightningCodecs.writeByte(psigs.size, out)
psigs.forEach {
LightningCodecs.writeU32(it.feerate.toLong().toInt(), out)
LightningCodecs.writeBytes(it.psig.partialSig, out)
LightningCodecs.writeBytes(it.psig.nonce.toByteArray(), out)
}
}

companion object : TlvValueReader<AlternativeFeeratePartialSigs> {
const val tag: Long = 0x47010003
override fun read(input: Input): AlternativeFeeratePartialSigs {
val count = LightningCodecs.byte(input)
val sigs = (0 until count).map {
AlternativeFeeratePartialSig(
FeeratePerKw(LightningCodecs.u32(input).toLong().sat),
PartialSignatureWithNonce(
LightningCodecs.bytes(input, 32).byteVector32(),
IndividualNonce(LightningCodecs.bytes(input, 66))
)
)
}
return AlternativeFeeratePartialSigs(sigs)
}
}
}

}

sealed class RevokeAndAckTlv : Tlv {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1259,9 +1259,12 @@ data class CommitSig(
override fun withNonEmptyChannelData(ecd: EncryptedChannelData): CommitSig = copy(tlvStream = tlvStream.addOrUpdate(CommitSigTlv.ChannelData(ecd)))

val alternativeFeerateSigs: List<CommitSigTlv.AlternativeFeerateSig> = tlvStream.get<CommitSigTlv.AlternativeFeerateSigs>()?.sigs ?: listOf()
val alternativeFeeratePartialSigs: List<CommitSigTlv.AlternativeFeeratePartialSig> = tlvStream.get<CommitSigTlv.AlternativeFeeratePartialSigs>()?.psigs ?: listOf()
val batchSize: Int = tlvStream.get<CommitSigTlv.Batch>()?.size ?: 1
val partialSig = tlvStream.get<CommitSigTlv.PartialSignatureWithNonceTlv>()?.psig
val sigOrPartialSig: Either<ByteVector64, PartialSignatureWithNonce> = partialSig?.let { Either.Right(it) } ?: Either.Left(signature)
val alternativFeerateSigsOrPartialSigs: List<Either<CommitSigTlv.AlternativeFeerateSig, CommitSigTlv.AlternativeFeeratePartialSig>> =
partialSig?.let { alternativeFeeratePartialSigs.map { Either.Right(it) } } ?: alternativeFeerateSigs.map { Either.Left(it) }

override fun write(out: Output) {
LightningCodecs.writeBytes(channelId, out)
Expand All @@ -1278,6 +1281,7 @@ data class CommitSig(
val readers = mapOf(
CommitSigTlv.ChannelData.tag to CommitSigTlv.ChannelData.Companion as TlvValueReader<CommitSigTlv>,
CommitSigTlv.AlternativeFeerateSigs.tag to CommitSigTlv.AlternativeFeerateSigs.Companion as TlvValueReader<CommitSigTlv>,
CommitSigTlv.AlternativeFeeratePartialSigs.tag to CommitSigTlv.AlternativeFeeratePartialSigs.Companion as TlvValueReader<CommitSigTlv>,
CommitSigTlv.Batch.tag to CommitSigTlv.Batch.Companion as TlvValueReader<CommitSigTlv>,
CommitSigTlv.PartialSignatureWithNonceTlv.tag to CommitSigTlv.PartialSignatureWithNonceTlv.Companion as TlvValueReader<CommitSigTlv>,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fr.acinq.lightning.channel

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.flatMap
import fr.acinq.lightning.*
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.lightning.blockchain.*
Expand Down Expand Up @@ -408,9 +410,13 @@ object TestsHelper {
return s1 to remoteCommitPublished
}

fun useAlternativeCommitSig(s: LNChannel<ChannelState>, commitment: Commitment, alternative: CommitSigTlv.AlternativeFeerateSig): Transaction {
fun useAlternativeCommitSig(s: LNChannel<ChannelState>, commitment: Commitment, alternative: Either<CommitSigTlv.AlternativeFeerateSig, CommitSigTlv.AlternativeFeeratePartialSig>): Transaction {
val channelKeys = s.commitments.params.localParams.channelKeys(s.ctx.keyManager)
val alternativeSpec = commitment.localCommit.spec.copy(feerate = alternative.feerate)
val feerate = when (alternative) {
is Either.Left -> alternative.value.feerate
is Either.Right -> alternative.value.feerate
}
val alternativeSpec = commitment.localCommit.spec.copy(feerate = feerate)
val fundingTxIndex = commitment.fundingTxIndex
val commitInput = commitment.commitInput
val remoteFundingPubKey = commitment.remoteFundingPubkey
Expand All @@ -426,10 +432,23 @@ object TestsHelper {
localPerCommitmentPoint,
alternativeSpec
)
val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex))
val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localSig, alternative.sig)
assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess)
return signedCommitTx.tx
return when (alternative) {
is Either.Left -> {
val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex))
val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localSig, alternative.value.sig)
assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess)
signedCommitTx.tx
}

is Either.Right -> {
val remoteSig = alternative.value.psig
val localNonce = channelKeys.verificationNonce(fundingTxIndex, commitment.localCommit.index)
val signed = Transactions.partialSign(localCommitTx, channelKeys.fundingKey(fundingTxIndex), channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localNonce, alternative.value.psig.nonce)
.flatMap { localSig -> Transactions.aggregatePartialSignatures(localCommitTx, localSig, remoteSig.partialSig, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localNonce.second, remoteSig.nonce) }
.map { aggSig -> Transactions.addAggregatedSignature(localCommitTx, aggSig) }
signed.right!!.tx
}
}
}

fun signAndRevack(alice: LNChannel<ChannelState>, bob: LNChannel<ChannelState>): Pair<LNChannel<ChannelState>, LNChannel<ChannelState>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ class ClosingTestsCommon : LightningTestSuite() {
val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(commitSigAlice))
val revBob = actionsBob6.hasOutgoingMessage<RevokeAndAck>()
val (alice6, _) = alice5.process(ChannelCommand.MessageReceived(revBob))
val alternativeCommitTx = useAlternativeCommitSig(alice6, alice6.commitments.active.first(), commitSigBob.alternativeFeerateSigs.first())
val alternativeCommitTx = useAlternativeCommitSig(alice6, alice6.commitments.active.first(), commitSigBob.alternativFeerateSigsOrPartialSigs.first())
remoteClose(alternativeCommitTx, bob6)
}

Expand Down Expand Up @@ -1055,7 +1055,7 @@ class ClosingTestsCommon : LightningTestSuite() {
val (bob4, actionsBob4) = bob3.process(ChannelCommand.Commitment.Sign)
val commitSigBob = actionsBob4.hasOutgoingMessage<CommitSig>()
val (alice4, _) = alice3.process(ChannelCommand.MessageReceived(commitSigBob))
val alternativeCommitTx = useAlternativeCommitSig(alice4, alice4.commitments.active.first(), commitSigBob.alternativeFeerateSigs.first())
val alternativeCommitTx = useAlternativeCommitSig(alice4, alice4.commitments.active.first(), commitSigBob.alternativFeerateSigsOrPartialSigs.first())
remoteClose(alternativeCommitTx, bob4)
}

Expand Down
Loading

0 comments on commit 76e23a4

Please sign in to comment.