Skip to content

Commit

Permalink
[WIP] Implement simple taproot channels
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Jun 6, 2024
1 parent 7e8482b commit bb004f9
Show file tree
Hide file tree
Showing 15 changed files with 1,123 additions and 121 deletions.
12 changes: 10 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object SimpleTaprootStaging : Feature() {
override val rfcName get() = "option_simple_taproot_staging"
override val mandatory get() = 180
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}
}

@Serializable
Expand Down Expand Up @@ -337,7 +343,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelBackupClient,
Feature.ChannelBackupProvider,
Feature.ExperimentalSplice,
Feature.Quiescence
Feature.Quiescence,
Feature.SimpleTaprootStaging
)

operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())
Expand Down Expand Up @@ -369,7 +376,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.TrampolinePayment to listOf(Feature.PaymentSecret),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret)
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.SimpleTaprootStaging to listOf(Feature.AnchorOutputs, Feature.StaticRemoteKey)
)

class FeatureException(message: String) : IllegalArgumentException(message)
Expand Down
3 changes: 2 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ data class NodeParams(
Feature.PayToOpenClient to FeatureSupport.Optional,
Feature.ChannelBackupClient to FeatureSupport.Optional,
Feature.ExperimentalSplice to FeatureSupport.Optional,
Feature.Quiescence to FeatureSupport.Mandatory
Feature.Quiescence to FeatureSupport.Mandatory,
Feature.SimpleTaprootStaging to FeatureSupport.Optional
),
dustLimit = 546.sat,
maxRemoteDustLimit = 600.sat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ sealed class ChannelAction {
is Transactions.TransactionWithInputInfo.CommitTx -> Type.CommitTx
is Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -> Type.HtlcSuccessTx
is Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx -> Type.HtlcTimeoutTx
is Transactions.TransactionWithInputInfo.HtlcDelayedTx -> Type.ClaimHtlcTimeoutTx
is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx -> Type.ClaimHtlcSuccessTx
is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx -> Type.ClaimHtlcTimeoutTx
is Transactions.TransactionWithInputInfo.ClaimAnchorOutputTx.ClaimLocalAnchorOutputTx -> Type.ClaimLocalAnchorOutputTx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ sealed class ChannelType {
override val features: Set<Feature> get() = setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels)
}

object SimpleTaprootStaging : SupportedChannelType() {
override val name: String get() = "simple_taproot_staging"
override val features: Set<Feature> get() = setOf(Feature.SimpleTaprootStaging, Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels)
}
}

data class UnsupportedChannelType(val featureBits: Features) : ChannelType() {
Expand All @@ -71,6 +75,7 @@ sealed class ChannelType {
// NB: Bolt 2: features must exactly match in order to identify a channel type.
fun fromFeatures(features: Features): ChannelType = when (features) {
// @formatter:off
Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory, Feature.SimpleTaprootStaging to FeatureSupport.Optional) -> SupportedChannelType.SimpleTaprootStaging
Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputsZeroReserve
Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputs
else -> UnsupportedChannelType(features)
Expand Down
39 changes: 26 additions & 13 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package fr.acinq.lightning.channel

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.Crypto.sha256
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.Try
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.Feature
import fr.acinq.lightning.Features
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
Expand Down Expand Up @@ -46,6 +48,8 @@ data class ChannelParams(
require(channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) { "FundingPubKeyBasedChannelKeyPath option must be enabled" }
}

val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging)

fun updateFeatures(localInit: Init, remoteInit: Init) = this.copy(
localParams = localParams.copy(features = localInit.features),
remoteParams = remoteParams.copy(features = remoteInit.features)
Expand Down Expand Up @@ -94,6 +98,7 @@ data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges:

data class HtlcTxAndSigs(val txinfo: HtlcTx, val localSig: ByteVector64, val remoteSig: ByteVector64)
data class PublishableTxs(val commitTx: CommitTx, val htlcTxsAndSigs: List<HtlcTxAndSigs>)
data class PartialSignatureWithNonce(val partialSig: ByteVector32, val nonce: IndividualNonce)

/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */
data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) {
Expand Down Expand Up @@ -254,9 +259,9 @@ data class Commitment(
val balanceNoFees = (reduced.toRemote - localChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat)
return if (params.localParams.isInitiator) {
// The initiator always pays the on-chain fees, so we must subtract that from the amount we can send.
val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced)
val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
// the initiator needs to keep a "initiator fee buffer" (see explanation above)
val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2)
val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2)
val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer)
if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(params.remoteParams.dustLimit, reduced).toMilliSatoshi()) {
// htlc will be trimmed
Expand All @@ -283,9 +288,9 @@ data class Commitment(
balanceNoFees
} else {
// The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive.
val commitFees = commitTxFeeMsat(params.localParams.dustLimit, reduced)
val commitFees = commitTxFeeMsat(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
// we expected the initiator to keep a "initiator fee buffer" (see explanation above)
val initiatorFeeBuffer = commitTxFeeMsat(params.localParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2)
val initiatorFeeBuffer = commitTxFeeMsat(params.localParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2)
val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer)
if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(params.localParams.dustLimit, reduced).toMilliSatoshi()) {
// htlc will be trimmed
Expand Down Expand Up @@ -351,10 +356,10 @@ data class Commitment(
val outgoingHtlcs = reduced.htlcs.incomings()

// note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment
val fees = commitTxFee(params.remoteParams.dustLimit, reduced)
val fees = commitTxFee(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
// the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid
// getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728).
val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2)
val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2)
// NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold)
// which may result in a lower commit tx fee; this is why we take the max of the two.
val missingForSender = reduced.toRemote - localChannelReserve(params).toMilliSatoshi() - (if (params.localParams.isInitiator) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat)
Expand Down Expand Up @@ -403,7 +408,7 @@ data class Commitment(
val incomingHtlcs = reduced.htlcs.incomings()

// note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment
val fees = commitTxFee(params.localParams.dustLimit, reduced)
val fees = commitTxFee(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
// NB: we don't enforce the initiatorFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place
// We could enforce it once we're confident a large portion of the network implements it.
val missingForSender = reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi() - (if (params.localParams.isInitiator) 0.sat else fees).toMilliSatoshi()
Expand Down Expand Up @@ -436,7 +441,7 @@ data class Commitment(
val reduced = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed)
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
// we look from remote's point of view, so if local is initiator remote doesn't pay the fees
val fees = commitTxFee(params.remoteParams.dustLimit, reduced)
val fees = commitTxFee(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
val missing = reduced.toRemote.truncateToSatoshi() - localChannelReserve(params) - fees
return if (missing < 0.sat) {
Either.Left(CannotAffordFees(params.channelId, -missing, localChannelReserve(params), fees))
Expand All @@ -453,7 +458,7 @@ data class Commitment(
// It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid,
// and it would be tricky to check if the conditions are met at signing
// (it also means that we need to check the fee of the initial commitment tx somewhere)
val fees = commitTxFee(params.localParams.dustLimit, reduced)
val fees = commitTxFee(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging))
val missing = reduced.toRemote.truncateToSatoshi() - remoteChannelReserve(params) - fees
return if (missing < 0.sat) {
Either.Left(CannotAffordFees(params.channelId, -missing, remoteChannelReserve(params), fees))
Expand Down Expand Up @@ -554,6 +559,7 @@ data class FullCommitment(
params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat
else -> (fundingAmount / 100).max(params.localParams.dustLimit)
}
val isTaprootChannel = params.isTaprootChannel
}

data class WaitingForRevocation(val sentAfterLocalCommitIndex: Long)
Expand All @@ -575,6 +581,7 @@ data class Commitments(
val channelId: ByteVector32 = params.channelId
val localNodeId: PublicKey = params.localParams.nodeId
val remoteNodeId: PublicKey = params.remoteParams.nodeId
val isTaprootChannel = params.isTaprootChannel

// Commitment numbers are the same for all active commitments.
val localCommitIndex = active.first().localCommit.index
Expand Down Expand Up @@ -982,6 +989,7 @@ data class Commitments(

val ANCHOR_AMOUNT = 330.sat
const val COMMIT_WEIGHT = 1124
const val COMMIT_WEIGHT_TAPROOT = 968
const val HTLC_OUTPUT_WEIGHT = 172
const val HTLC_TIMEOUT_WEIGHT = 666
const val HTLC_SUCCESS_WEIGHT = 706
Expand Down Expand Up @@ -1028,6 +1036,7 @@ data class Commitments(
val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint)
val localRevocationPubkey = remoteParams.revocationBasepoint.deriveForRevocation(localPerCommitmentPoint)
val localPaymentBasepoint = channelKeys.paymentBasepoint
val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging)
val outputs = makeCommitTxOutputs(
channelKeys.fundingPubKey(fundingTxIndex),
remoteFundingPubKey,
Expand All @@ -1039,10 +1048,12 @@ data class Commitments(
remotePaymentPubkey,
localHtlcPubkey,
remoteHtlcPubkey,
spec
spec,
isTaprootChannel
)

val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isInitiator, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs, isTaprootChannel)
return Pair(commitTx, htlcTxs)
}

Expand All @@ -1062,6 +1073,7 @@ data class Commitments(
val remoteDelayedPaymentPubkey = remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint)
val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint)
val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint)
val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging)
val outputs = makeCommitTxOutputs(
remoteFundingPubKey,
channelKeys.fundingPubKey(fundingTxIndex),
Expand All @@ -1073,11 +1085,12 @@ data class Commitments(
localPaymentPubkey,
remoteHtlcPubkey,
localHtlcPubkey,
spec
spec,
isTaprootChannel
)
// NB: we are creating the remote commit tx, so local/remote parameters are inverted.
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isInitiator, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs, isTaprootChannel)
return Pair(commitTx, htlcTxs)
}
}
Expand Down
Loading

0 comments on commit bb004f9

Please sign in to comment.