Skip to content

Commit a7b505f

Browse files
committed
Update splicing protocol
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). Similar to how commitment points are handled, `firstRemoteNonce` and `secondRemoteNonce` fields have been added to `SpliceInit` and `SpliceAck`, encoded as a list of nonces (instead of 2 expicit nonces) We also need a 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. The life-cycle of these nonces is tied to the life-cycle of the interactive session which is never persisted (nonces here do not have to be deterministic).
1 parent 7f04b49 commit a7b505f

30 files changed

+632
-198
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
121121

122122
commitmentFormat match {
123123
case Transactions.DefaultCommitmentFormat => networkFeerate
124-
case _: Transactions.AnchorOutputsCommitmentFormat =>
124+
case _: Transactions.AnchorOutputsCommitmentFormat =>
125125
val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
126126
// We make sure the feerate is always greater than the propagation threshold.
127127
targetFeerate.max(networkMinFee * 1.25)

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,6 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
594594
remotePushAmount: MilliSatoshi,
595595
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
596596
deferred: Option[CommitSig],
597-
remoteNextLocalNonce: Option[IndividualNonce],
598597
replyTo_opt: Option[akka.actor.typed.ActorRef[Peer.OpenChannelResponse]]) extends TransientChannelData
599598
final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams,
600599
secondRemotePerCommitmentPoint: PublicKey,

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,16 @@ object ChannelTypes {
131131
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
132132
override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
133133
}
134-
case object SimpleTaprootChannelsStaging extends SupportedChannelType {
134+
case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
135135
/** Known channel-type features */
136136
override def features: Set[ChannelTypeFeature] = Set(
137+
if (scidAlias) Some(Features.ScidAlias) else None,
138+
if (zeroConf) Some(Features.ZeroConf) else None,
137139
Some(Features.SimpleTaprootStaging)
138140
).flatten
139-
140-
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
141141
override def paysDirectlyToWallet: Boolean = false
142-
/** Format of the channel transactions. */
143142
override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelsStagingCommitmentFormat
144-
145-
override def toString: String = "simple_taproot_channel_staging"
143+
override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
146144
}
147145

148146
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
@@ -168,7 +166,11 @@ object ChannelTypes {
168166
AnchorOutputsZeroFeeHtlcTx(zeroConf = true),
169167
AnchorOutputsZeroFeeHtlcTx(scidAlias = true),
170168
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true),
171-
SimpleTaprootChannelsStaging)
169+
SimpleTaprootChannelsStaging(),
170+
SimpleTaprootChannelsStaging(zeroConf = true),
171+
SimpleTaprootChannelsStaging(scidAlias = true),
172+
SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
173+
)
172174
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
173175
.toMap
174176

@@ -184,7 +186,7 @@ object ChannelTypes {
184186
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
185187
val zeroConf = canUse(Features.ZeroConf)
186188
if (canUse(Features.SimpleTaprootStaging)) {
187-
SimpleTaprootChannelsStaging
189+
SimpleTaprootChannelsStaging(scidAlias, zeroConf)
188190
} else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
189191
AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf)
190192
} else if (canUse(Features.AnchorOutputs)) {

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package fr.acinq.eclair.channel
22

3-
import akka.event.LoggingAdapter
4-
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
3+
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
4+
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
55
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
66
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Musig2, Satoshi, SatoshiLong, Script, Transaction, TxId}
77
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf}
@@ -17,6 +17,7 @@ import fr.acinq.eclair.transactions.Transactions._
1717
import fr.acinq.eclair.transactions._
1818
import fr.acinq.eclair.wire.protocol._
1919
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, payment}
20+
import grizzled.slf4j.Logging
2021
import scodec.bits.ByteVector
2122

2223
/** Static channel parameters shared by all commitments. */
@@ -226,11 +227,23 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig:
226227
object LocalCommit {
227228
def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId,
228229
fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
229-
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = {
230+
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, LocalCommit] = {
230231
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
231232
if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
232233
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
233234
}
235+
commit.sigOrPartialSig match {
236+
case Left(_) =>
237+
if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
238+
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
239+
}
240+
case Right(psig) =>
241+
val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey
242+
val Some(localNonce) = localNonce_opt
243+
if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) {
244+
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
245+
}
246+
}
234247
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
235248
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
236249
return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
@@ -249,7 +262,7 @@ object LocalCommit {
249262

250263
/** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */
251264
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) {
252-
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce]): CommitSig = {
265+
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = {
253266
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec)
254267
val (sig, tlvStream) = params.commitmentFormat match {
255268
case SimpleTaprootChannelsStagingCommitmentFormat =>
@@ -650,6 +663,7 @@ case class Commitment(fundingTxIndex: Long,
650663
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
651664
val Some(remoteNonce) = nextRemoteNonce_opt
652665
val Right(psig) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
666+
log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint")
653667
Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
654668
case _ =>
655669
Set.empty
@@ -665,11 +679,12 @@ case class Commitment(fundingTxIndex: Long,
665679
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set(
666680
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None
667681
).flatten[CommitSigTlv] ++ partialSig))
682+
log.debug(s"sendCommit: setting remoteNextPerCommitmentPoint to $remoteNextPerCommitmentPoint")
668683
val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
669684
(copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig)
670685
}
671686

672-
def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = {
687+
def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig, localNonce_opt: Option[(SecretNonce, IndividualNonce)])(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = {
673688
// they sent us a signature for *their* view of *our* next commit tx
674689
// so in terms of rev.hashes and indexes we have:
675690
// ourCommit.index -> our current revocation hash, which is about to become our old revocation hash
@@ -680,7 +695,7 @@ case class Commitment(fundingTxIndex: Long,
680695
// and will increment our index
681696
val localCommitIndex = localCommit.index + 1
682697
val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed)
683-
LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint).map { localCommit1 =>
698+
LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint, localNonce_opt).map { localCommit1 =>
684699
log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.commitTxAndRemoteSig.commitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","))
685700
copy(localCommit = localCommit1)
686701
}
@@ -695,9 +710,10 @@ case class Commitment(fundingTxIndex: Long,
695710
addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig)
696711
case Right(remotePartialSigWithNonce) =>
697712
val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex)
698-
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, ChannelKeyManager.keyPath(fundingPubKey.publicKey), localCommit.index)
713+
val channelKeyPath = ChannelKeyManager.keyPath(keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0L))
714+
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, channelKeyPath, localCommit.index)
699715
val Right(partialSig) = keyManager.partialSign(unsignedCommitTx,
700-
keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), remoteFundingPubKey,
716+
keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey,
701717
TxOwner.Local,
702718
localNonce, remotePartialSigWithNonce.nonce)
703719
val Right(aggSig) = Musig2.aggregateTaprootSignatures(
@@ -1034,11 +1050,17 @@ case class Commitments(params: ChannelParams,
10341050
}
10351051
}
10361052

1037-
def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonce_opt: Option[IndividualNonce] = None)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = {
1053+
def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonces: List[IndividualNonce] = List.empty)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = {
10381054
remoteNextCommitInfo match {
10391055
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
10401056
case Right(remoteNextPerCommitmentPoint) =>
1041-
val (active1, sigs) = active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteNonce_opt)).unzip
1057+
val (active1, sigs) = this.params.commitmentFormat match {
1058+
case SimpleTaprootChannelsStagingCommitmentFormat =>
1059+
require(active.size <= nextRemoteNonces.size, s"we have ${active.size} commitments but ${nextRemoteNonces.size} remote musig2 nonces")
1060+
active.zip(nextRemoteNonces).map { case (c, n) => c.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, Some(n)) } unzip
1061+
case _ =>
1062+
active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, None)).unzip
1063+
}
10421064
val commitments1 = copy(
10431065
changes = changes.copy(
10441066
localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed),
@@ -1063,7 +1085,11 @@ case class Commitments(params: ChannelParams,
10631085
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
10641086
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
10651087
val active1 = active.zip(commits).map { case (commitment, commit) =>
1066-
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match {
1088+
val localNonce_opt = params.commitmentFormat match {
1089+
case SimpleTaprootChannelsStagingCommitmentFormat => Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, commitment.fundingTxIndex, channelKeyPath, localCommitIndex + 1))
1090+
case _ => None
1091+
}
1092+
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit, localNonce_opt) match {
10671093
case Left(f) => return Left(f)
10681094
case Right(commitment1) => commitment1
10691095
}
@@ -1073,9 +1099,12 @@ case class Commitments(params: ChannelParams,
10731099
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2)
10741100
val tlvStream: TlvStream[RevokeAndAckTlv] = params.commitmentFormat match {
10751101
case SimpleTaprootChannelsStagingCommitmentFormat =>
1076-
val (_, nonce) = keyManager.verificationNonce(params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
1077-
log.debug("generating our next local nonce with {} {} {} {}", params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
1078-
TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce))
1102+
val nonces = this.active.map(c => {
1103+
val n = keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
1104+
log.debug(s"revokeandack: creating verification nonce ${n._2} fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}")
1105+
n
1106+
})
1107+
TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList))
10791108
case _ =>
10801109
TlvStream.empty
10811110
}
@@ -1100,7 +1129,7 @@ case class Commitments(params: ChannelParams,
11001129
remoteNextCommitInfo match {
11011130
case Right(_) => Left(UnexpectedRevocation(channelId))
11021131
case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId))
1103-
case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonce_opt.isEmpty => Left(MissingNextLocalNonce(channelId))
1132+
case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId))
11041133
case Left(_) =>
11051134
// Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment.
11061135
val receivedHtlcs = changes.remoteChanges.signed.collect {

0 commit comments

Comments
 (0)