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).
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).
  • Loading branch information
sstone committed Jun 12, 2024
1 parent 63b4d50 commit 0082aea
Show file tree
Hide file tree
Showing 30 changed files with 646 additions and 231 deletions.
15 changes: 15 additions & 0 deletions eclair-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@
<version>4.1.94.Final</version>
</dependency>
<!-- BITCOIN -->
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.20.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jvm</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jni-jvm</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>fr.acinq</groupId>
<artifactId>bitcoin-lib_${scala.version.short}</artifactId>
Expand Down
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 @@ -569,14 +569,15 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
remotePushAmount: MilliSatoshi,
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
deferred: Option[CommitSig],
remoteNextLocalNonce: Option[IndividualNonce],
secondRemoteNonce_opt: Option[IndividualNonce],
replyTo_opt: Option[akka.actor.typed.ActorRef[Peer.OpenChannelResponse]]) extends TransientChannelData
final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams,
secondRemotePerCommitmentPoint: PublicKey,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
signingSession: InteractiveTxSigningSession.WaitingForSigs,
remoteChannelData_opt: Option[ByteVector]) extends ChannelDataWithoutCommitments
remoteChannelData_opt: Option[ByteVector],
secondRemoteNonce_opt: Option[IndividualNonce]) extends ChannelDataWithoutCommitments
final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
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
@@ -1,7 +1,7 @@
package fr.acinq.eclair.channel

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

/** Static channel parameters shared by all commitments. */
Expand Down Expand Up @@ -229,11 +230,23 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig:
object LocalCommit {
def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId,
fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = {
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, LocalCommit] = {
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
}
commit.sigOrPartialSig match {
case Left(_) =>
if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
}
case Right(psig) =>
val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey
val Some(localNonce) = localNonce_opt
if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) {
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
}
}
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
Expand All @@ -252,7 +265,7 @@ object LocalCommit {

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

def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = {
def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig, localNonce_opt: Option[(SecretNonce, IndividualNonce)])(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = {
// they sent us a signature for *their* view of *our* next commit tx
// so in terms of rev.hashes and indexes we have:
// ourCommit.index -> our current revocation hash, which is about to become our old revocation hash
Expand All @@ -683,7 +698,7 @@ case class Commitment(fundingTxIndex: Long,
// and will increment our index
val localCommitIndex = localCommit.index + 1
val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed)
LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint).map { localCommit1 =>
LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint, localNonce_opt).map { localCommit1 =>
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(","))
copy(localCommit = localCommit1)
}
Expand All @@ -698,9 +713,10 @@ case class Commitment(fundingTxIndex: Long,
addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig)
case Right(remotePartialSigWithNonce) =>
val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex)
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, ChannelKeyManager.keyPath(fundingPubKey.publicKey), localCommit.index)
val channelKeyPath = ChannelKeyManager.keyPath(keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0L))
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, channelKeyPath, 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 +1053,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 All @@ -1066,7 +1088,11 @@ case class Commitments(params: ChannelParams,
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
val active1 = active.zip(commits).map { case (commitment, commit) =>
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match {
val localNonce_opt = params.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat => Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, commitment.fundingTxIndex, channelKeyPath, localCommitIndex + 1))
case _ => None
}
commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit, localNonce_opt) match {
case Left(f) => return Left(f)
case Right(commitment1) => commitment1
}
Expand All @@ -1076,9 +1102,12 @@ 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)
log.debug("generating our next local nonce with {} {} {} {}", params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce))
val nonces = this.active.map(c => {
val n = keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2)
log.debug(s"revokeandack: creating verification nonce ${n._2} fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}")
n
})
TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList))
case _ =>
TlvStream.empty
}
Expand All @@ -1103,7 +1132,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
Loading

0 comments on commit 0082aea

Please sign in to comment.