Skip to content

Commit

Permalink
Update splicing protocol
Browse files Browse the repository at this point in the history
The current "simple taproot channels" proposal is not compatible with splices.
Supporting splices means supporting multiple commitment transactions that are valid at the same time, with the same commitment index but with different funding transactions.
We need to extend the taproot proposal to include a list of musig2 nonces (one for each active commitment transaction), see lightning/bolts#995 (comment)).
We also need a new "next remote nonce" for the new commit tx that is being built, here it has been added to `SpliceInit` and `SpliceAck`.

The funding tx that is being built during the interactive session needs to spend the current funding tx.
For this, we re-use the scheme that we developped for our custome "swaproot" musig swap-ins: we add musig2 nonces to the `TxComplete` message, one nonce for each input that requires one, ordered by serial id.
  • Loading branch information
sstone committed May 23, 2024
1 parent faf8e85 commit f556839
Show file tree
Hide file tree
Showing 29 changed files with 528 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,

commitmentFormat match {
case Transactions.DefaultCommitmentFormat => networkFeerate
case _: Transactions.AnchorOutputsCommitmentFormat =>
case _: Transactions.AnchorOutputsCommitmentFormat =>
val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
// We make sure the feerate is always greater than the propagation threshold.
targetFeerate.max(networkMinFee * 1.25)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ object SpliceStatus {
/** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */
case object NonInitiatorQuiescent extends QuiescentSpliceStatus
/** We told our peer we want to splice funds in the channel. */
case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends QuiescentSpliceStatus
case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit, nonce_opt: Option[(SecretNonce, IndividualNonce)]) extends QuiescentSpliceStatus
/** We both agreed to splice and are building the splice transaction. */
case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], sessionId: ByteVector32, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends QuiescentSpliceStatus
/** The splice transaction has been negotiated, we're exchanging signatures. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,16 @@ object ChannelTypes {
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
}
case object SimpleTaprootChannelsStaging extends SupportedChannelType {
case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
/** Known channel-type features */
override def features: Set[ChannelTypeFeature] = Set(
if (scidAlias) Some(Features.ScidAlias) else None,
if (zeroConf) Some(Features.ZeroConf) else None,
Some(Features.SimpleTaprootStaging)
).flatten

/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
override def paysDirectlyToWallet: Boolean = false
/** Format of the channel transactions. */
override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelsStagingCommitmentFormat

override def toString: String = "simple_taproot_channel_staging"
override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
}

case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
Expand All @@ -168,7 +166,11 @@ object ChannelTypes {
AnchorOutputsZeroFeeHtlcTx(zeroConf = true),
AnchorOutputsZeroFeeHtlcTx(scidAlias = true),
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true),
SimpleTaprootChannelsStaging)
SimpleTaprootChannelsStaging(),
SimpleTaprootChannelsStaging(zeroConf = true),
SimpleTaprootChannelsStaging(scidAlias = true),
SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
)
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
.toMap

Expand All @@ -184,7 +186,7 @@ object ChannelTypes {
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
val zeroConf = canUse(Features.ZeroConf)
if (canUse(Features.SimpleTaprootStaging)) {
SimpleTaprootChannelsStaging
SimpleTaprootChannelsStaging(scidAlias, zeroConf)
} else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf)
} else if (canUse(Features.AnchorOutputs)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ case class Commitment(fundingTxIndex: Long,
val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex)
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, ChannelKeyManager.keyPath(fundingPubKey.publicKey), localCommit.index)
val Right(partialSig) = keyManager.partialSign(unsignedCommitTx,
keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), remoteFundingPubKey,
keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey,
TxOwner.Local,
localNonce, remotePartialSigWithNonce.nonce)
val Right(aggSig) = Musig2.aggregateTaprootSignatures(
Expand Down Expand Up @@ -1037,11 +1037,17 @@ case class Commitments(params: ChannelParams,
}
}

def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonce_opt: Option[IndividualNonce] = None)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = {
def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonces: List[IndividualNonce] = List.empty)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = {
remoteNextCommitInfo match {
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
case Right(remoteNextPerCommitmentPoint) =>
val (active1, sigs) = active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteNonce_opt)).unzip
val (active1, sigs) = this.params.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat =>
require(active.size <= nextRemoteNonces.size, s"we have ${active.size} commitments but ${nextRemoteNonces.size} remote musig2 nonces")
active.zip(nextRemoteNonces).map { case (c, n) => c.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, Some(n)) } unzip
case _ =>
active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, None)).unzip
}
val commitments1 = copy(
changes = changes.copy(
localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed),
Expand Down Expand Up @@ -1076,9 +1082,9 @@ case class Commitments(params: ChannelParams,
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2)
val tlvStream: TlvStream[RevokeAndAckTlv] = params.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat =>
val (_, nonce) = keyManager.verificationNonce(params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
val nonces = this.active.map(c => keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2))
log.debug("generating our next local nonce with {} {} {} {}", params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce))
TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList))
case _ =>
TlvStream.empty
}
Expand All @@ -1103,7 +1109,7 @@ case class Commitments(params: ChannelParams,
remoteNextCommitInfo match {
case Right(_) => Left(UnexpectedRevocation(channelId))
case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId))
case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonce_opt.isEmpty => Left(MissingNextLocalNonce(channelId))
case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId))
case Left(_) =>
// Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment.
val receivedHtlcs = changes.remoteChanges.signed.collect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,8 @@ object Helpers {
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommitIndex + 1)
val tlvStream: TlvStream[RevokeAndAckTlv] = commitments.params.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat =>
val (_, nonce) = keyManager.verificationNonce(commitments.params.localParams.fundingKeyPath, commitments.latest.fundingTxIndex, channelKeyPath, commitments.localCommitIndex + 1)
TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce))
val nonces = commitments.active.map(c => keyManager.verificationNonce(commitments.params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, commitments.localCommitIndex + 1))
TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList))
case _ =>
TlvStream.empty
}
Expand Down
Loading

0 comments on commit f556839

Please sign in to comment.