Skip to content

Commit 0ce1aa6

Browse files
committed
Add a new commitment format for taproot channels that use the old (and unsafe) anchor output format
1 parent a7b505f commit 0ce1aa6

File tree

17 files changed

+772
-811
lines changed

17 files changed

+772
-811
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2020
import fr.acinq.bitcoin.scalacompat.Satoshi
2121
import fr.acinq.eclair.BlockHeight
2222
import fr.acinq.eclair.transactions.Transactions
23-
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, SimpleTaprootChannelsStagingCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
23+
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, SimpleTaprootChannelsStagingCommitmentFormat, SimpleTaprootChannelsStagingLegacyCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
2424

2525
// @formatter:off
2626
sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] {
@@ -77,15 +77,15 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
7777
def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
7878
commitmentFormat match {
7979
case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
80-
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelsStagingCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
80+
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelsStagingCommitmentFormat | SimpleTaprootChannelsStagingLegacyCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
8181
}
8282
}
8383

8484
def isProposedFeerateTooLow(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
8585
commitmentFormat match {
8686
case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow
8787
// When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time.
88-
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelsStagingCommitmentFormat => false
88+
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelsStagingCommitmentFormat | SimpleTaprootChannelsStagingLegacyCommitmentFormat => false
8989
}
9090
}
9191
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ case class ChannelFeatures(features: Set[PermanentChannelFeature]) {
3434
val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) && !hasFeature((Features.SimpleTaprootStaging))
3535
/** Legacy option_anchor_outputs is used for Phoenix, because Phoenix doesn't have an on-chain wallet to pay for fees. */
3636
val commitmentFormat: CommitmentFormat = if (hasFeature(Features.SimpleTaprootStaging)) {
37-
SimpleTaprootChannelsStagingCommitmentFormat
37+
if (hasFeature(Features.AnchorOutputs)) SimpleTaprootChannelsStagingLegacyCommitmentFormat
38+
else SimpleTaprootChannelsStagingCommitmentFormat
3839
} else if (hasFeature(Features.AnchorOutputs)) {
3940
UnsafeLegacyAnchorOutputsCommitmentFormat
4041
} else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
@@ -136,7 +137,7 @@ object ChannelTypes {
136137
override def features: Set[ChannelTypeFeature] = Set(
137138
if (scidAlias) Some(Features.ScidAlias) else None,
138139
if (zeroConf) Some(Features.ZeroConf) else None,
139-
Some(Features.SimpleTaprootStaging)
140+
Some(Features.SimpleTaprootStaging),
140141
).flatten
141142
override def paysDirectlyToWallet: Boolean = false
142143
override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelsStagingCommitmentFormat

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

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,15 @@ object LocalCommit {
264264
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) {
265265
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = {
266266
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec)
267-
val (sig, tlvStream) = params.commitmentFormat match {
268-
case SimpleTaprootChannelsStagingCommitmentFormat =>
269-
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), index)
270-
val Some(remoteNonce) = remoteNonce_opt
271-
val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
272-
val tlvStream: TlvStream[CommitSigTlv] = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2)))
273-
(ByteVector64.Zeroes, tlvStream)
274-
case _ =>
275-
val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat)
276-
(sig, TlvStream[CommitSigTlv]())
267+
val (sig, tlvStream) = if (params.commitmentFormat.useTaproot) {
268+
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), index)
269+
val Some(remoteNonce) = remoteNonce_opt
270+
val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
271+
val tlvStream: TlvStream[CommitSigTlv] = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2)))
272+
(ByteVector64.Zeroes, tlvStream)
273+
} else {
274+
val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat)
275+
(sig, TlvStream[CommitSigTlv]())
277276
}
278277
val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig)
279278
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
@@ -651,22 +650,19 @@ case class Commitment(fundingTxIndex: Long,
651650
// remote commitment will include all local proposed changes + remote acked changes
652651
val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed)
653652
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remoteNextPerCommitmentPoint, spec)
654-
val sig = params.commitmentFormat match {
655-
case SimpleTaprootChannelsStagingCommitmentFormat =>
656-
ByteVector64.Zeroes
657-
case _ =>
658-
keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat)
653+
val sig = if (params.commitmentFormat.useTaproot) {
654+
ByteVector64.Zeroes
655+
} else {
656+
keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat)
659657
}
660-
val partialSig: Set[CommitSigTlv] = params.commitmentFormat match {
661-
case SimpleTaprootChannelsStagingCommitmentFormat =>
662-
// we generate a new nonce each time we sign their commit tx
663-
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
664-
val Some(remoteNonce) = nextRemoteNonce_opt
665-
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")
667-
Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
668-
case _ =>
669-
Set.empty
658+
val partialSig: Set[CommitSigTlv] = if (params.commitmentFormat.useTaproot) {
659+
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
660+
val Some(remoteNonce) = nextRemoteNonce_opt
661+
val Right(psig) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
662+
log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint")
663+
Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
664+
} else {
665+
Set.empty
670666
}
671667
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index)
672668
val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig)
@@ -1054,12 +1050,11 @@ case class Commitments(params: ChannelParams,
10541050
remoteNextCommitInfo match {
10551051
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
10561052
case Right(remoteNextPerCommitmentPoint) =>
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
1053+
val (active1, sigs) = if (this.params.commitmentFormat.useTaproot) {
1054+
require(active.size <= nextRemoteNonces.size, s"we have ${active.size} commitments but ${nextRemoteNonces.size} remote musig2 nonces")
1055+
active.zip(nextRemoteNonces).map { case (c, n) => c.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, Some(n)) } unzip
1056+
} else {
1057+
active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, None)).unzip
10631058
}
10641059
val commitments1 = copy(
10651060
changes = changes.copy(
@@ -1085,9 +1080,10 @@ case class Commitments(params: ChannelParams,
10851080
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
10861081
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
10871082
val active1 = active.zip(commits).map { case (commitment, commit) =>
1088-
val localNonce_opt = params.commitmentFormat match {
1089-
case SimpleTaprootChannelsStagingCommitmentFormat => Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, commitment.fundingTxIndex, channelKeyPath, localCommitIndex + 1))
1090-
case _ => None
1083+
val localNonce_opt = if (params.commitmentFormat.useTaproot) {
1084+
Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, commitment.fundingTxIndex, channelKeyPath, localCommitIndex + 1))
1085+
} else {
1086+
None
10911087
}
10921088
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit, localNonce_opt) match {
10931089
case Left(f) => return Left(f)
@@ -1097,16 +1093,15 @@ case class Commitments(params: ChannelParams,
10971093
// we will send our revocation preimage + our next revocation hash
10981094
val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, localCommitIndex)
10991095
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2)
1100-
val tlvStream: TlvStream[RevokeAndAckTlv] = params.commitmentFormat match {
1101-
case SimpleTaprootChannelsStagingCommitmentFormat =>
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))
1108-
case _ =>
1109-
TlvStream.empty
1096+
val tlvStream: TlvStream[RevokeAndAckTlv] = if (params.commitmentFormat.useTaproot) {
1097+
val nonces = this.active.map(c => {
1098+
val n = keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
1099+
log.debug(s"revokeandack: creating verification nonce ${n._2} fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}")
1100+
n
1101+
})
1102+
TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList))
1103+
} else {
1104+
TlvStream.empty
11101105
}
11111106
val revocation = RevokeAndAck(
11121107
channelId = channelId,
@@ -1129,7 +1124,7 @@ case class Commitments(params: ChannelParams,
11291124
remoteNextCommitInfo match {
11301125
case Right(_) => Left(UnexpectedRevocation(channelId))
11311126
case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId))
1132-
case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId))
1127+
case Left(_) if this.params.commitmentFormat.useTaproot && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId))
11331128
case Left(_) =>
11341129
// Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment.
11351130
val receivedHtlcs = changes.remoteChanges.signed.collect {
@@ -1221,9 +1216,10 @@ case class Commitments(params: ChannelParams,
12211216
active.forall { commitment =>
12221217
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
12231218
val remoteFundingKey = commitment.remoteFundingPubKey
1224-
val fundingScript = params.commitmentFormat match {
1225-
case SimpleTaprootChannelsStagingCommitmentFormat => Script.write(Scripts.musig2FundingScript(localFundingKey, remoteFundingKey))
1226-
case _ => Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey))
1219+
val fundingScript = if (params.commitmentFormat.useTaproot) {
1220+
Script.write(Scripts.musig2FundingScript(localFundingKey, remoteFundingKey))
1221+
} else {
1222+
Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey))
12271223
}
12281224
commitment.commitInput.redeemScriptOrScriptTree == Left(fundingScript)
12291225
}

0 commit comments

Comments
 (0)