What changed, and why it matters
This is a large feature commit that adds support for a new kind of Bitcoin Lightning channel using Taproot and MuSig2 signatures. It changes how channels are opened, spliced, re-established after disconnections, and closed. The commit is primarily a protocol upgrade, not a stated security fix. However, because it touches sensitive signing logic and introduces new nonce handling, there is a moderate risk that mistakes in the new code could affect channel safety or allow a malicious peer to cause problems. The commit message does not describe this as fixing a known vulnerability.
Treat this as a high-complexity protocol upgrade rather than an urgent security patch. Reviewers should focus on MuSig2 nonce lifecycle correctness: ensure nonces are never reused, are correctly bound to funding transactions and commitment numbers, are validated before partial signatures are accepted, and are properly persisted and retransmitted after reconnection. Pay special attention to splice channel-type upgrades and the new reconnection logic for partially signed transactions. Run the expanded test suite and consider additional fuzzing or cross-implementation interop testing.
Security signals we found
New cryptographic signing path using MuSig2 partial signatures and nonces
New validation exceptions for missing or invalid commit/funding/closing nonces
Protocol message extensions for nonce exchange during channel lifecycle
Changes to reconnection retransmission logic for commit_sig and tx_signatures
Support for upgrading channel commitment format during splice operations
No explicit security bug fix or vulnerability disclosure in commit message
Evidence from the diff
The commit implements simple Taproot channels for Eclair, introducing new commitment formats (PhoenixSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat), new feature bits, MuSig2 nonce exchange in channel establishment v1/v2, splice_init/splice_ack channel_type TLV for upgrading Phoenix anchor channels to Taproot, and nonce fields in revoke_and_ack, channel_reestablish, shutdown, closing_complete, and closing_sig messages. The diff shows substantial changes to signature aggregation, partial signature verification, and reconnection/retransmission logic. Several new exception types are added for missing/invalid nonces. There is no vendor disclosure of a security issue or CVE in the commit message or diff.
Changed components
eclair-core channel state machine (Channel.scala)commitment and signature logic (Commitments.scala, Helpers.scala, Transactions.scala, Scripts.scala)interactive transaction builder and signing session (InteractiveTxBuilder.scala, InteractiveTxSigningSession)wire protocol codecs and TLVs (ChannelTlv.scala, InteractiveTxTlv.scala, LightningMessageTypes.scala, LightningMessageCodecs.scala)channel establishment FSMs (ChannelOpenSingleFunded.scala, ChannelOpenDualFunded.scala)closing negotiation logic (NEGOTIATING_SIMPLE state)feature definitions (Features.scala, ChannelFeatures.scala)Eclair API splice methods (Eclair.scala)Inspect captured patch +3465 / −896
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
index 2310e35..8c1080d 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
@@ -47,6 +47,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.{ClearRecipient, OfferPayment, PaymentIdentifier}
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router._
+import fr.acinq.eclair.transactions.Transactions.CommitmentFormat
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
import fr.acinq.eclair.wire.protocol._
import grizzled.slf4j.Logging
@@ -96,9 +97,9 @@ trait Eclair {
def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
- def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
+ def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
- def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
+ def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
@@ -260,15 +261,15 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
)
}
- override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
+ override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))
sendToChannelTyped(
channel = Left(channelId),
- cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None)
+ cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = channelType_opt)
)
}
- override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
+ override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
val script = scriptOrAddress match {
case Left(script) => script
case Right(address) => addressToPublicKeyScript(this.appKit.nodeParams.chainHash, address) match {
@@ -279,7 +280,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
val spliceOut = SpliceOut(amount = amountOut, scriptPubKey = script)
sendToChannelTyped(
channel = Left(channelId),
- cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None)
+ cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None, channelType_opt = channelType_opt)
)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
index f594e78..2716434 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
@@ -341,6 +341,16 @@ object Features {
val mandatory = 154
}
+ case object SimpleTaprootChannelsPhoenix extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
+ val rfcName = "option_simple_taproot_phoenix"
+ val mandatory = 564
+ }
+
+ case object SimpleTaprootChannelsStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
+ val rfcName = "option_simple_taproot_staging"
+ val mandatory = 180
+ }
+
/**
* Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md.
* TODO: add NodeFeature once bLIP is merged.
@@ -384,6 +394,8 @@ object Features {
ZeroConf,
KeySend,
SimpleClose,
+ SimpleTaprootChannelsPhoenix,
+ SimpleTaprootChannelsStaging,
WakeUpNotificationClient,
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
@@ -403,6 +415,8 @@ object Features {
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil),
SimpleClose -> (ShutdownAnySegwit :: Nil),
+ SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
+ SimpleTaprootChannelsStaging -> (ChannelType :: SimpleClose :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
index 6ba1dd6..0c8b958 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
@@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Satoshi
import fr.acinq.eclair.BlockHeight
import fr.acinq.eclair.transactions.Transactions
-import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions._
// @formatter:off
sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] {
@@ -76,8 +76,8 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
commitmentFormat match {
- case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
- case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
+ case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
+ case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
}
}
@@ -85,7 +85,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
commitmentFormat match {
case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow
// When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time.
- case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false
+ case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false
}
}
}
@@ -122,7 +122,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
commitmentFormat match {
case Transactions.DefaultCommitmentFormat => networkFeerate
- case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat=>
+ case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat =>
val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
// We make sure the feerate is always greater than the propagation threshold.
targetFeerate.max(networkMinFee * 1.25)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala
index ccead6c..95b4602 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala
@@ -260,7 +260,7 @@ sealed trait ChannelFundingCommand extends Command {
}
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
-final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends ChannelFundingCommand {
+final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding], channelType_opt:Option[ChannelType]) extends ChannelFundingCommand {
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala
index bbfc31b..8169685 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala
@@ -154,4 +154,9 @@ case class ConcurrentRemoteSplice (override val channelId: Byte
case class TooManySmallHtlcs (override val channelId: ByteVector32, number: Long, below: MilliSatoshi) extends ChannelJammingException(channelId, s"too many small htlcs: $number HTLCs below $below")
case class IncomingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"incoming confidence too low: confidence=$confidence occupancy=$occupancy")
case class OutgoingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"outgoing confidence too low: confidence=$confidence occupancy=$occupancy")
+case class MissingCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is missing")
+case class InvalidCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is not valid")
+case class MissingFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is missing")
+case class InvalidFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is not valid")
+case class MissingClosingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "closing nonce is missing")
// @formatter:on
\ No newline at end of file
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
index dc38e86..ad4799e 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala
@@ -16,7 +16,7 @@
package fr.acinq.eclair.channel
-import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature}
/**
@@ -118,6 +118,29 @@ 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 class SimpleTaprootChannelsPhoenix(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.SimpleTaprootChannelsPhoenix),
+ ).flatten
+ override def paysDirectlyToWallet: Boolean = false
+ override def commitmentFormat: CommitmentFormat = PhoenixSimpleTaprootChannelCommitmentFormat
+ override def toString: String = s"simple_taproot_channel_phoenix${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
+ }
+ 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.SimpleTaprootChannelsStaging),
+ ).flatten
+ override def paysDirectlyToWallet: Boolean = false
+ override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat
+ 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 {
override def features: Set[InitFeature] = featureBits.activated.keySet
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
@@ -140,7 +163,16 @@ object ChannelTypes {
AnchorOutputsZeroFeeHtlcTx(),
AnchorOutputsZeroFeeHtlcTx(zeroConf = true),
AnchorOutputsZeroFeeHtlcTx(scidAlias = true),
- AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true))
+ AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true),
+ SimpleTaprootChannelsPhoenix(),
+ SimpleTaprootChannelsPhoenix(zeroConf = true),
+ SimpleTaprootChannelsPhoenix(scidAlias = true),
+ SimpleTaprootChannelsPhoenix(scidAlias = true, zeroConf = true),
+ SimpleTaprootChannelsStaging(),
+ SimpleTaprootChannelsStaging(zeroConf = true),
+ SimpleTaprootChannelsStaging(scidAlias = true),
+ SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
+ )
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
.toMap
@@ -153,7 +185,11 @@ object ChannelTypes {
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
val zeroConf = canUse(Features.ZeroConf)
- if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
+ if (canUse(Features.SimpleTaprootChannelsStaging)) {
+ SimpleTaprootChannelsStaging(scidAlias, zeroConf)
+ } else if (canUse(Features.SimpleTaprootChannelsPhoenix)) {
+ SimpleTaprootChannelsPhoenix(scidAlias, zeroConf)
+ } else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf)
} else if (canUse(Features.AnchorOutputs)) {
AnchorOutputs(scidAlias, zeroConf)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
index 729f989..9ef5d0a 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
@@ -5,11 +5,12 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxId}
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf}
+import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce}
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.channel.fsm.Channel.ChannelConf
-import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
+import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
import fr.acinq.eclair.payment.OutgoingPaymentPacket
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.Announcements
@@ -171,16 +172,17 @@ object LocalCommit {
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Either[ChannelException, LocalCommit] = {
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(channelParams, commitParams, commitKeys, localCommitIndex, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec)
val remoteCommitSigOk = commitmentFormat match {
- case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(commit.signature))
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, commit.signature)
+ case _: SimpleTaprootChannelCommitmentFormat => commit.sigOrPartialSig match {
+ case _: IndividualSignature => false
+ case remoteSig: PartialSignatureWithNonce =>
+ val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommitIndex)
+ localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, remoteFundingPubKey, remoteSig, localNonce.publicNonce)
+ }
}
if (!remoteCommitSigOk) {
return Left(InvalidCommitmentSignature(channelParams.channelId, fundingTxId, localCommitIndex, localCommitTx.tx))
}
- val commitTxRemoteSig = commitmentFormat match {
- case _: SegwitV0CommitmentFormat => ChannelSpendSignature.IndividualSignature(commit.signature)
- case _: SimpleTaprootChannelCommitmentFormat => ???
- }
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
return Left(HtlcSigCountMismatch(channelParams.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
@@ -192,13 +194,13 @@ object LocalCommit {
}
remoteSig
}
- Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commitTxRemoteSig, htlcRemoteSigs))
+ Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.sigOrPartialSig, htlcRemoteSigs))
}
}
/** 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(channelParams: ChannelParams, commitParams: CommitParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, commitmentFormat: CommitmentFormat): CommitSig = {
+ def sign(channelParams: ChannelParams, commitParams: CommitParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, commitmentFormat: CommitmentFormat, remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, CommitSig] = {
val fundingKey = channelKeys.fundingKey(fundingTxIndex)
val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentPoint, commitmentFormat)
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(channelParams, commitParams, commitKeys, index, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec)
@@ -206,9 +208,18 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePer
val htlcSigs = sortedHtlcTxs.map(_.localSig(commitKeys))
commitmentFormat match {
case _: SegwitV0CommitmentFormat =>
- val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig
- CommitSig(channelParams.channelId, sig, htlcSigs.toList)
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
+ Right(CommitSig(channelParams.channelId, sig, htlcSigs.toList))
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ remoteNonce_opt match {
+ case Some(remoteNonce) =>
+ val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, commitInput.outPoint.txid)
+ remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
+ case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
+ case Right(psig) => Right(CommitSig(channelParams.channelId, psig, htlcSigs.toList, batchSize = 1))
+ }
+ case None => Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
+ }
}
}
}
@@ -646,28 +657,31 @@ case class Commitment(fundingTxIndex: Long,
Right(())
}
- def sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int)(implicit log: LoggingAdapter): (Commitment, CommitSig) = {
+ def sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, nextRemoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): Either[ChannelException, (Commitment, CommitSig)] = {
// remote commitment will include all local proposed changes + remote acked changes
val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed)
val fundingKey = localFundingKey(channelKeys)
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, remoteCommitParams, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, spec)
val htlcSigs = htlcTxs.sortBy(_.input.outPoint.index).map(_.localSig(commitKeys))
-
// NB: IN/OUT htlcs are inverted because this is the remote commit
log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","))
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)
-
- val tlvs = Set(
- if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None
- ).flatten[CommitSigTlv]
- val commitSig = commitmentFormat match {
- case _: SegwitV0CommitmentFormat =>
- val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig
- CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs))
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ val sig = commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ nextRemoteNonce_opt match {
+ case Some(remoteNonce) =>
+ val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, fundingTxId)
+ remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
+ case Left(_) => return Left(InvalidCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1))
+ case Right(psig) => psig
+ }
+ case None => return Left(MissingCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1))
+ }
}
+ val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, batchSize)
val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
- (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig)
+ Right((copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig))
}
def receiveCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, changes: CommitmentChanges, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = {
@@ -694,10 +708,21 @@ case class Commitment(fundingTxIndex: Long,
val commitKeys = localKeys(params, channelKeys)
val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec)
localCommit.remoteSig match {
- case remoteSig: ChannelSpendSignature.IndividualSignature =>
+ case remoteSig: IndividualSignature =>
val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey)
unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig)
- case _: ChannelSpendSignature.PartialSignatureWithNonce => ???
+ case remoteSig: PartialSignatureWithNonce =>
+ val localNonce = if (fundingTxIndex == 0 && localCommit.index == 0 && !params.channelFeatures.hasFeature(Features.DualFunding)) {
+ // With channel establishment v1, we exchange the first nonce before the funding tx and remote funding key are known.
+ NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, localCommit.index)
+ } else {
+ NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommit.index)
+ }
+ // We have already validated the remote nonce and partial signature when we received it, so we're guaranteed
+ // that the following code cannot produce an error.
+ val Right(localSig) = unsignedCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce))
+ val Right(signedTx) = unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig)
+ signedTx
}
}
@@ -1061,13 +1086,16 @@ case class Commitments(channelParams: ChannelParams,
}
}
- def sendCommit(channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = {
+ def sendCommit(channelKeys: ChannelKeys, nextRemoteCommitNonces: Map[TxId, IndividualNonce])(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = {
remoteNextCommitInfo match {
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
case Right(remoteNextPerCommitmentPoint) =>
val (active1, sigs) = active.map(c => {
val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remoteNextPerCommitmentPoint, c.commitmentFormat)
- c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size)
+ c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteCommitNonces.get(c.fundingTxId)) match {
+ case Left(e) => return Left(e)
+ case Right((c, cs)) => (c, cs)
+ }
}).unzip
val commitments1 = copy(
changes = changes.copy(
@@ -1103,10 +1131,17 @@ case class Commitments(channelParams: ChannelParams,
// we will send our revocation preimage + our next revocation hash
val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex)
val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2)
+ val localCommitNonces = active.flatMap(c => c.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => None
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localNonce = NonceGenerator.verificationNonce(c.fundingTxId, c.localFundingKey(channelKeys), c.remoteFundingPubKey, localCommitIndex + 2)
+ Some(c.fundingTxId -> localNonce.publicNonce)
+ })
val revocation = RevokeAndAck(
channelId = channelId,
perCommitmentSecret = localPerCommitmentSecret,
- nextPerCommitmentPoint = localNextPerCommitmentPoint
+ nextPerCommitmentPoint = localNextPerCommitmentPoint,
+ nextCommitNonces = localCommitNonces,
)
val commitments1 = copy(
changes = changes.copy(
@@ -1123,6 +1158,9 @@ case class Commitments(channelParams: ChannelParams,
remoteNextCommitInfo match {
case Right(_) => Left(UnexpectedRevocation(channelId))
case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId))
+ case Left(_) if active.exists(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)) =>
+ val missingNonce = active.find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)).get
+ Left(MissingCommitNonce(channelId, missingNonce.fundingTxId, remoteCommitIndex + 1))
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 {
@@ -1223,15 +1261,6 @@ case class Commitments(channelParams: ChannelParams,
}
}
- /** This function should be used to ignore a commit_sig that we've already received. */
- def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = {
- val isLatestSig = latest.localCommit.remoteSig match {
- case ChannelSpendSignature.IndividualSignature(latestRemoteSig) => latestRemoteSig == commitSig.signature
- case ChannelSpendSignature.PartialSignatureWithNonce(_, _) => ???
- }
- channelParams.channelFeatures.hasFeature(Features.DualFunding) && isLatestSig
- }
-
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
index 23e6ba4..a1643e3 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
@@ -17,15 +17,18 @@
package fr.acinq.eclair.channel
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256}
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.OnChainPubkeyCache
import fr.acinq.eclair.blockchain.fee._
+import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce}
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
-import fr.acinq.eclair.crypto.ShaChain
+import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
+import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Announcements
@@ -131,6 +134,10 @@ object Helpers {
}
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
+ channelType.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => ()
+ }
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, open.fundingSatoshis)
@@ -238,6 +245,10 @@ object Helpers {
if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio))
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
+ channelType.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => ()
+ }
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
}
@@ -536,10 +547,18 @@ object Helpers {
// they just sent a new commit_sig, we have received it but they didn't receive our revocation
val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1)
val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1)
+ val localCommitNonces = commitments.active.flatMap(c => c.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => None
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val fundingKey = channelKeys.fundingKey(c.fundingTxIndex)
+ val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, c.remoteFundingPubKey, commitments.localCommitIndex + 1).publicNonce
+ Some(c.fundingTxId -> n)
+ })
val revocation = RevokeAndAck(
channelId = commitments.channelId,
perCommitmentSecret = localPerCommitmentSecret,
- nextPerCommitmentPoint = localNextPerCommitmentPoint
+ nextPerCommitmentPoint = localNextPerCommitmentPoint,
+ nextCommitNonces = localCommitNonces,
)
checkRemoteCommit(remoteChannelReestablish, retransmitRevocation_opt = Some(revocation))
} else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) {
@@ -563,6 +582,17 @@ object Helpers {
}
}
+ def checkCommitNonces(channelReestablish: ChannelReestablish, commitments: Commitments, pendingSig_opt: Option[InteractiveTxSigningSession.WaitingForSigs]): Option[ChannelException] = {
+ pendingSig_opt match {
+ case Some(pendingSig) if pendingSig.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(pendingSig.fundingTxId) =>
+ Some(MissingCommitNonce(commitments.channelId, pendingSig.fundingTxId, commitments.remoteCommitIndex + 1))
+ case _ =>
+ commitments.active
+ .find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(c.fundingTxId))
+ .map(c => MissingCommitNonce(commitments.channelId, c.fundingTxId, commitments.remoteCommitIndex + 1))
+ }
+ }
+
}
object Closing {
@@ -674,7 +704,7 @@ object Helpers {
// this is just to estimate the weight, it depends on size of the pubkey scripts
val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, 0 sat, 0 sat, commitment.localCommit.spec)
val dummyPubkey = commitment.remoteFundingPubKey
- val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig)
+ val dummySig = IndividualSignature(Transactions.PlaceHolderSig)
val closingWeight = dummyClosingTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig).weight()
log.info(s"using feerates=$feerates for initial closing tx")
feerates.computeFees(closingWeight)
@@ -719,8 +749,8 @@ object Helpers {
val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee))
if (checkClosingDustAmounts(closingTx)) {
val fundingPubkey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey
- if (closingTx.checkRemoteSig(fundingPubkey, commitment.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(remoteClosingSig))) {
- val signedTx = closingTx.aggregateSigs(fundingPubkey, commitment.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(closingSigned.signature), ChannelSpendSignature.IndividualSignature(remoteClosingSig))
+ if (closingTx.checkRemoteSig(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(remoteClosingSig))) {
+ val signedTx = closingTx.aggregateSigs(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(closingSigned.signature), IndividualSignature(remoteClosingSig))
Right(closingTx.copy(tx = signedTx), closingSigned)
} else {
Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid))
@@ -731,17 +761,23 @@ object Helpers {
}
/** We are the closer: we sign closing transactions for which we pay the fees. */
- def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
+ def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, (ClosingTxs, ClosingComplete, CloserNonces)] = {
// We must convert the feerate to a fee: we must build dummy transactions to compute their weight.
val commitInput = commitment.commitInput(channelKeys)
val closingFee = {
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
dummyClosingTxs.preferred_opt match {
case Some(dummyTx) =>
- val dummyPubkey = commitment.remoteFundingPubKey
- val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig)
- val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig)
- SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight()))
+ commitment.commitmentFormat match {
+ case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat =>
+ val dummyPubkey = commitment.remoteFundingPubKey
+ val dummySig = IndividualSignature(Transactions.PlaceHolderSig)
+ val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig)
+ SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight()))
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val dummySignedTx = dummyTx.tx.updateWitness(dummyTx.inputIndex, Script.witnessKeyPathPay2tr(Transactions.PlaceHolderSig))
+ SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight()))
+ }
case None => return Left(CannotGenerateClosingTx(commitment.channelId))
}
}
@@ -752,12 +788,33 @@ object Helpers {
case _ => return Left(CannotGenerateClosingTx(commitment.channelId))
}
val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
- val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, TlvStream(Set(
- closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
- closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
- closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
- ).flatten[ClosingTlv]))
- Right(closingTxs, closingComplete)
+ val localNonces = CloserNonces.generate(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId)
+ val tlvs: TlvStream[ClosingCompleteTlv] = commitment.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ remoteNonce_opt match {
+ case None => return Left(MissingClosingNonce(commitment.channelId))
+ case Some(remoteNonce) =>
+ // If we cannot create our partial signature for one of our closing txs, we just skip it.
+ // It will only happen if our peer sent an invalid nonce, in which case we cannot do anything anyway
+ // apart from eventually force-closing.
+ def localSig(tx: ClosingTx, localNonce: LocalNonce): Option[PartialSignatureWithNonce] = {
+ tx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)).toOption
+ }
+
+ TlvStream(Set(
+ closingTxs.localAndRemote_opt.flatMap(tx => localSig(tx, localNonces.localAndRemote)).map(ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(_)),
+ closingTxs.localOnly_opt.flatMap(tx => localSig(tx, localNonces.localOnly)).map(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(_)),
+ closingTxs.remoteOnly_opt.flatMap(tx => localSig(tx, localNonces.remoteOnly)).map(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(_)),
+ ).flatten[ClosingCompleteTlv])
+ }
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => TlvStream(Set(
+ closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
+ closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
+ closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
+ ).flatten[ClosingCompleteTlv])
+ }
+ val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, tlvs)
+ Right(closingTxs, closingComplete, localNonces)
}
/**
@@ -766,35 +823,70 @@ object Helpers {
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they
* are not using our latest script (race condition between our closing_complete and theirs).
*/
- def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
+ def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localNonce_opt: Option[LocalNonce]): Either[ChannelException, (ClosingTx, ClosingSig, Option[LocalNonce])] = {
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey)
// If our output isn't dust, they must provide a signature for a transaction that includes it.
// Note that we're the closee, so we look for signatures including the closee output.
- (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
- case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
- case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
- case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
- case _ => ()
- }
- // We choose the closing signature that matches our preferred closing transaction.
- val closingTxsWithSigs = Seq(
- closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))),
- closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))),
- closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))),
- ).flatten
- closingTxsWithSigs.headOption match {
- case Some((closingTx, remoteSig, sigToTlv)) =>
- val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
- val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey)
- val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig))
- val signedClosingTx = closingTx.copy(tx = signedTx)
- if (signedClosingTx.validate(extraUtxos = Map.empty)) {
- Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))))
- } else {
- Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
+ commitment.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat => localNonce_opt match {
+ case None => Left(MissingClosingNonce(commitment.channelId))
+ case Some(localNonce) =>
+ (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
+ case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty && closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case (Some(_), None) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case (None, Some(_)) if closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case _ => ()
+ }
+ // We choose the closing signature that matches our preferred closing transaction.
+ val closingTxsWithSigs = Seq(
+ closingComplete.closerAndCloseeOutputsPartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(localSig)))),
+ closingComplete.closeeOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloseeOutputOnlyPartialSignature(localSig)))),
+ closingComplete.closerOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig)))),
+ ).flatten
+ closingTxsWithSigs.headOption match {
+ case Some((closingTx, remoteSig, sigToTlv)) =>
+ val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
+ val signedClosingTx_opt = for {
+ localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption
+ signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption
+ } yield (closingTx.copy(tx = signedTx), localSig.partialSig)
+ signedClosingTx_opt match {
+ case Some((signedClosingTx, localSig)) if signedClosingTx.validate(extraUtxos = Map.empty) =>
+ val nextLocalNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId)
+ val tlvs = TlvStream[ClosingSigTlv](sigToTlv(localSig), ClosingSigTlv.NextCloseeNonce(nextLocalNonce.publicNonce))
+ Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, tlvs), Some(nextLocalNonce))
+ case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid))
+ }
+ case None => Left(MissingCloseSignature(commitment.channelId))
+ }
+ }
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat =>
+ (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
+ case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
+ case _ => ()
+ }
+ // We choose the closing signature that matches our preferred closing transaction.
+ val closingTxsWithSigs = Seq(
+ closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))),
+ closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))),
+ closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))),
+ ).flatten
+ closingTxsWithSigs.headOption match {
+ case Some((closingTx, remoteSig, sigToTlv)) =>
+ val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
+ val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey)
+ val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, IndividualSignature(remoteSig))
+ val signedClosingTx = closingTx.copy(tx = signedTx)
+ if (signedClosingTx.validate(extraUtxos = Map.empty)) {
+ Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))), None)
+ } else {
+ Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
+ }
+ case None => Left(MissingCloseSignature(commitment.channelId))
}
- case None => Left(MissingCloseSignature(commitment.channelId))
}
}
@@ -805,22 +897,38 @@ object Helpers {
* sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait
* for their next closing_sig that will match our latest closing_complete.
*/
- def receiveSimpleClosingSig(channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, ClosingTx] = {
+ def receiveSimpleClosingSig(channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig, localNonces_opt: Option[CloserNonces], remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, ClosingTx] = {
val closingTxsWithSig = Seq(
- closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))),
- closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))),
- closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))),
+ closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, IndividualSignature(sig)))),
+ closingSig.closerAndCloseeOutputsPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localAndRemote_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))),
+ closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, IndividualSignature(sig)))),
+ closingSig.closerOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))),
+ closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, IndividualSignature(sig)))),
+ closingSig.closeeOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.remoteOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce)))))
).flatten
closingTxsWithSig.headOption match {
case Some((closingTx, remoteSig)) =>
val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
- val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey)
- val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig))
- val signedClosingTx = closingTx.copy(tx = signedTx)
- if (signedClosingTx.validate(extraUtxos = Map.empty)) {
- Right(signedClosingTx)
- } else {
- Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
+ val signedClosingTx_opt = remoteSig match {
+ case remoteSig: IndividualSignature =>
+ val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey)
+ val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
+ Some(closingTx.copy(tx = signedTx))
+ case remoteSig: PartialSignatureWithNonce =>
+ val localNonce = localNonces_opt match {
+ case Some(localNonces) if closingTx.tx.txOut.size == 2 => localNonces.localAndRemote
+ case Some(localNonces) if closingTx.toLocalOutput_opt.nonEmpty => localNonces.localOnly
+ case Some(localNonces) => localNonces.remoteOnly
+ case None => return Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid))
+ }
+ for {
+ localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption
+ signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption
+ } yield closingTx.copy(tx = signedTx)
+ }
+ signedClosingTx_opt match {
+ case Some(signedClosingTx) if signedClosingTx.validate(extraUtxos = Map.empty) => Right(signedClosingTx)
+ case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid))
}
case None => Left(MissingCloseSignature(commitment.channelId))
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
index fd70b66..3e1d9b4 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter}
import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed}
import akka.event.Logging.MDC
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction, TxId}
import fr.acinq.eclair.Logs.LogCategory
@@ -40,6 +41,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxFunder, InteractiveTxSigningSession}
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishReplaceableTx, SetChannelId}
import fr.acinq.eclair.channel.publish._
+import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.crypto.keymanager.ChannelKeys
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType
import fr.acinq.eclair.db.PendingCommandsDb
@@ -49,7 +51,7 @@ import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain}
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.Announcements
-import fr.acinq.eclair.transactions.Transactions.ClosingTx
+import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.protocol._
@@ -220,6 +222,15 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
import Channel._
+ // Remote nonces that must be used when signing the next remote commitment transaction (one per active commitment).
+ var remoteNextCommitNonces: Map[TxId, IndividualNonce] = Map.empty
+
+ // Closee nonces are first exchanged in shutdown messages, and replaced by a new nonce after each closing_sig.
+ var localCloseeNonce_opt: Option[LocalNonce] = None
+ var remoteCloseeNonce_opt: Option[IndividualNonce] = None
+ // Closer nonces are randomly generated when sending our closing_complete.
+ var localCloserNonces_opt: Option[CloserNonces] = None
+
// we pass these to helpers classes so that they have the logging context
implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog
@@ -623,7 +634,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.debug("ignoring CMD_SIGN (nothing to sign)")
stay()
case Right(_) =>
- d.commitments.sendCommit(channelKeys) match {
+ d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match {
case Right((commitments1, commit)) =>
log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String)
val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit
@@ -678,14 +689,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1)
}
}
- case (_, sig: CommitSig) if d.commitments.ignoreRetransmittedCommitSig(sig) =>
- // If our peer hasn't implemented https://github.com/lightning/bolts/pull/1214, they may retransmit commit_sig
- // even though we've already received it and haven't requested a retransmission. It is safe to simply ignore
- // this commit_sig while we wait for peers to correctly implemented commit_sig retransmission, at which point
- // we should be able to get rid of this edge case.
- // Note that the funding transaction may have confirmed while we were reconnecting.
- log.info("ignoring commit_sig, we're still waiting for tx_signatures")
- stay()
case _ =>
// NB: in all other cases we process the commit_sigs normally. We could do a full pattern matching on all
// splice statuses, but it would force us to handle every corner case where our peer doesn't behave correctly
@@ -725,6 +728,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match {
case Right((commitments1, actions)) =>
cancelTimer(RevocationTimeout.toString)
+ remoteNextCommitNonces = revocation.nextCommitNonces
log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String)
actions.foreach {
case PostRevocationAction.RelayHtlc(add) =>
@@ -745,7 +749,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
if (d.remoteShutdown.isDefined && !commitments1.changes.localHasUnsignedOutgoingHtlcs) {
// we were waiting for our pending htlcs to be signed before replying with our local shutdown
val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d)
- val localShutdown = Shutdown(d.channelId, finalScriptPubKey)
+ val localShutdown = createShutdown(d.commitments, finalScriptPubKey)
// this should always be defined, we provide a fallback for backward compat with older channels
val closeStatus = d.closeStatus_opt.getOrElse(CloseStatus.NonInitiator(None))
// note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING
@@ -772,7 +776,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
d.commitments.channelParams.validateLocalShutdownScript(localScriptPubKey) match {
case Left(e) => handleCommandError(e, c)
case Right(localShutdownScript) =>
- val shutdown = Shutdown(d.channelId, localShutdownScript)
+ val shutdown = createShutdown(d.commitments, localShutdownScript)
handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closeStatus_opt = Some(CloseStatus.Initiator(c.feerates)))) storing() sending shutdown
}
}
@@ -798,6 +802,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// we did not send a shutdown message
// there are pending signed changes => go to SHUTDOWN
// there are no htlcs => go to NEGOTIATING
+ remoteCloseeNonce_opt = remoteShutdown.closeeNonce_opt
if (d.commitments.changes.remoteHasUnsignedOutgoingHtlcs) {
handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown))
} else if (d.commitments.changes.remoteHasUnsignedOutgoingUpdateFee) {
@@ -815,13 +820,14 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
// in the meantime we won't send new changes
stay() using d.copy(remoteShutdown = Some(remoteShutdown), closeStatus_opt = Some(CloseStatus.NonInitiator(None)))
+ } else if (d.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && remoteShutdown.closeeNonce_opt.isEmpty) {
+ handleLocalError(MissingClosingNonce(d.channelId), d, Some(remoteShutdown))
} else {
// so we don't have any unsigned outgoing changes
val (localShutdown, sendList) = d.localShutdown match {
- case Some(localShutdown) =>
- (localShutdown, Nil)
+ case Some(localShutdown) => (localShutdown, Nil)
case None =>
- val localShutdown = Shutdown(d.channelId, getOrGenerateFinalScriptPubKey(d))
+ val localShutdown = createShutdown(d.commitments, getOrGenerateFinalScriptPubKey(d))
// we need to send our shutdown if we didn't previously
(localShutdown, localShutdown :: Nil)
}
@@ -1090,22 +1096,34 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage)
} else {
val parentCommitment = d.commitments.latest.commitment
- val commitmentFormat = parentCommitment.commitmentFormat
val localFundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey
- val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, commitmentFormat).pubkeyScript
+ val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript
LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, msg.useFeeCredit_opt) match {
case Left(t) =>
log.warning("rejecting splice request with invalid liquidity ads: {}", t.getMessage)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage)
case Right(willFund_opt) =>
log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}")
+ // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the
+ // commitment format and will simply apply the previous commitment format.
+ val nextCommitmentFormat = msg.channelType_opt match {
+ case Some(channelType: ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat =>
+ log.info(s"accepting upgrade to $channelType during splice from commitment format ${parentCommitment.commitmentFormat}")
+ PhoenixSimpleTaprootChannelCommitmentFormat
+ case Some(channelType) =>
+ log.info(s"rejecting upgrade to $channelType during splice from commitment format ${parentCommitment.commitmentFormat}")
+ parentCommitment.commitmentFormat
+ case _ =>
+ parentCommitment.commitmentFormat
+ }
val spliceAck = SpliceAck(d.channelId,
fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(0 sat),
fundingPubKey = localFundingPubKey,
pushAmount = 0.msat,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
willFund_opt = willFund_opt.map(_.willFund),
- feeCreditUsed_opt = msg.useFeeCredit_opt
+ feeCreditUsed_opt = msg.useFeeCredit_opt,
+ channelType_opt = msg.channelType_opt
)
val fundingParams = InteractiveTxParams(
channelId = d.channelId,
@@ -1115,7 +1133,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = Nil,
- commitmentFormat = commitmentFormat,
+ commitmentFormat = nextCommitmentFormat,
lockTime = msg.lockTime,
dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit),
targetFeerate = msg.feerate,
@@ -1154,7 +1172,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case SpliceStatus.SpliceRequested(cmd, spliceInit) =>
log.info("our peer accepted our splice request and will contribute {} to the funding transaction", msg.fundingContribution)
val parentCommitment = d.commitments.latest.commitment
- val commitmentFormat = parentCommitment.commitmentFormat
+ // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the
+ // commitment format and will simply apply the previous commitment format.
+ val nextCommitmentFormat = msg.channelType_opt match {
+ case Some(_: ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat => PhoenixSimpleTaprootChannelCommitmentFormat
+ case _ => parentCommitment.commitmentFormat
+ }
val fundingParams = InteractiveTxParams(
channelId = d.channelId,
isInitiator = true,
@@ -1163,13 +1186,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = cmd.spliceOutputs,
- commitmentFormat = commitmentFormat,
+ commitmentFormat = nextCommitmentFormat,
lockTime = spliceInit.lockTime,
dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit),
targetFeerate = spliceInit.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs)
)
- val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, commitmentFormat).pubkeyScript
+ val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript
LiquidityAds.validateRemoteFunding(spliceInit.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, isChannelCreation = false, msg.willFund_opt) match {
case Left(t) =>
log.info("rejecting splice attempt: invalid liquidity ads response ({})", t.getMessage)
@@ -1377,8 +1400,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName)
stay()
}
- case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) =>
+ case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) =>
log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}")
+ nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) }
cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal))
remoteCommitSig_opt.foreach(self ! _)
liquidityPurchase_opt.collect {
@@ -1630,7 +1654,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.debug("ignoring CMD_SIGN (nothing to sign)")
stay()
case Right(_) =>
- d.commitments.sendCommit(channelKeys) match {
+ d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match {
case Right((commitments1, commit)) =>
log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String)
val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit
@@ -1662,7 +1686,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) {
- val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus)
+ val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus)
goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.toSeq
} else if (d.commitments.localChannelParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
@@ -1688,6 +1712,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match {
case Right((commitments1, actions)) =>
cancelTimer(RevocationTimeout.toString)
+ remoteNextCommitNonces = revocation.nextCommitNonces
log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String)
actions.foreach {
case PostRevocationAction.RelayHtlc(add) =>
@@ -1707,7 +1732,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) {
- val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus)
+ val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus)
goto(NEGOTIATING_SIMPLE) using d1 storing() sending closingComplete_opt.toSeq
} else if (d.commitments.localChannelParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
@@ -1730,6 +1755,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
if (shutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) {
log.debug("our peer updated their shutdown script (previous={}, current={})", d.remoteShutdown.scriptPubKey, shutdown.scriptPubKey)
}
+ remoteCloseeNonce_opt = shutdown.closeeNonce_opt
stay() using d.copy(remoteShutdown = shutdown) storing()
case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d)
@@ -1740,19 +1766,20 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) =>
val useSimpleClose = Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)
- val localShutdown_opt = c.scriptPubKey match {
- case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(Shutdown(d.channelId, scriptPubKey))
+ val nextScriptPubKey_opt = c.scriptPubKey match {
+ case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(scriptPubKey)
case _ => None
}
if (c.scriptPubKey.exists(_ != d.localShutdown.scriptPubKey) && !useSimpleClose) {
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
- } else if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) {
+ } else if (nextScriptPubKey_opt.nonEmpty || c.feerates.nonEmpty) {
val closeStatus1 = d.closeStatus match {
case initiator: CloseStatus.Initiator => initiator.copy(feerates_opt = c.feerates.orElse(initiator.feerates_opt))
case nonInitiator: CloseStatus.NonInitiator => nonInitiator.copy(feerates_opt = c.feerates.orElse(nonInitiator.feerates_opt)) // NB: this is the corner case where we can be non-initiator and have custom feerates
}
- val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closeStatus = closeStatus1)
- handleCommandSuccess(c, d1) storing() sending localShutdown_opt.toSeq
+ val shutdown = createShutdown(d.commitments, nextScriptPubKey_opt.getOrElse(d.localShutdown.scriptPubKey))
+ val d1 = d.copy(localShutdown = shutdown, closeStatus = closeStatus1)
+ handleCommandSuccess(c, d1) storing() sending shutdown
} else {
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
}
@@ -1871,6 +1898,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
when(NEGOTIATING_SIMPLE)(handleExceptions {
case Event(shutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) =>
+ remoteCloseeNonce_opt = shutdown.closeeNonce_opt
if (shutdown.scriptPubKey != d.remoteScriptPubKey) {
// This may lead to a signature mismatch: peers must use closing_complete to update their closing script.
log.warning("received shutdown changing remote script, this may lead to a signature mismatch: previous={}, current={}", d.remoteScriptPubKey, shutdown.scriptPubKey)
@@ -1886,10 +1914,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val err = InvalidRbfFeerate(d.channelId, closingFeerate, d.lastClosingFeerate * 1.2)
handleCommandError(err, c)
} else {
- MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate) match {
+ MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate, remoteCloseeNonce_opt) match {
case Left(f) => handleCommandError(f, c)
- case Right((closingTxs, closingComplete)) =>
+ case Right((closingTxs, closingComplete, closerNonces)) =>
log.debug("signing local mutual close transactions: {}", closingTxs)
+ localCloserNonces_opt = Some(closerNonces)
handleCommandSuccess(c, d.copy(lastClosingFeerate = closingFeerate, localScriptPubKey = localScript, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs)) storing() sending closingComplete
}
}
@@ -1902,12 +1931,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// No need to persist their latest script, they will re-sent it on reconnection.
stay() using d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey) sending Warning(d.channelId, InvalidCloseeScript(d.channelId, closingComplete.closeeScriptPubKey, d.localScriptPubKey).getMessage)
} else {
- MutualClose.signSimpleClosingTx(channelKeys, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete) match {
+ MutualClose.signSimpleClosingTx(channelKeys, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete, localCloseeNonce_opt) match {
case Left(f) =>
log.warning("invalid closing_complete: {}", f.getMessage)
stay() sending Warning(d.channelId, f.getMessage)
- case Right((signedClosingTx, closingSig)) =>
+ case Right((signedClosingTx, closingSig, nextCloseeNonce_opt)) =>
log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx)
+ localCloseeNonce_opt = nextCloseeNonce_opt
val d1 = d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx)
stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig
}
@@ -1917,13 +1947,15 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// Note that if we sent two closing_complete in a row, without waiting for their closing_sig for the first one,
// this will fail because we only care about our latest closing_complete. This is fine, we should receive their
// closing_sig for the last closing_complete afterwards.
- MutualClose.receiveSimpleClosingSig(channelKeys, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match {
+ MutualClose.receiveSimpleClosingSig(channelKeys, d.commitments.latest, d.proposedClosingTxs.last, closingSig, localCloserNonces_opt, remoteCloseeNonce_opt) match {
case Left(f) =>
log.warning("invalid closing_sig: {}", f.getMessage)
+ remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt
stay() sending Warning(d.channelId, f.getMessage)
case Right(signedClosingTx) =>
log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx)
val d1 = d.copy(publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx)
+ remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt
stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = true)
}
@@ -2265,8 +2297,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(c: CMD_CLOSE, d: DATA_CLOSING) => handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
case Event(c: CMD_BUMP_FORCE_CLOSE_FEE, d: DATA_CLOSING) =>
- d.commitments.latest.commitmentFormat match {
- case commitmentFormat: Transactions.AnchorOutputsCommitmentFormat =>
+ val commitmentFormat = d.commitments.latest.commitmentFormat
+ commitmentFormat match {
+ case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat =>
val commitment = d.commitments.latest
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
val localAnchor_opt = for {
@@ -2371,14 +2404,29 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) =>
activeConnection = r
val myFirstPerCommitmentPoint = channelKeys.commitmentPoint(0)
- val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId))
+ val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTxId))
+ val nonceTlvs = d.signingSession.fundingParams.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => Set.empty
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localFundingKey = channelKeys.fundingKey(0)
+ val remoteFundingPubKey = d.signingSession.fundingParams.remoteFundingPubKey
+ val currentCommitNonce_opt = d.signingSession.localCommit match {
+ case Left(_) => Some(NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 0))
+ case Right(_) => None
+ }
+ val nextCommitNonce = NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 1)
+ Set(
+ Some(ChannelReestablishTlv.NextLocalNoncesTlv(List(d.signingSession.fundingTxId -> nextCommitNonce.publicNonce))),
+ currentCommitNonce_opt.map(n => ChannelReestablishTlv.CurrentCommitNonceTlv(n.publicNonce)),
+ ).flatten[ChannelReestablishTlv]
+ }
val channelReestablish = ChannelReestablish(
channelId = d.channelId,
nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber,
nextRemoteRevocationNumber = 0,
yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
- TlvStream(nextFundingTlv),
+ TlvStream(nextFundingTlv ++ nonceTlvs),
)
val d1 = Helpers.updateFeatures(d, localInit, remoteInit)
goto(SYNCING) using d1 sending channelReestablish
@@ -2423,13 +2471,43 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet
} else Set.empty
+ // We send our verification nonces for all active commitments.
+ val nextCommitNonces: Map[TxId, IndividualNonce] = d.commitments.active.flatMap(c => {
+ c.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => None
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex)
+ Some(c.fundingTxId -> NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, c.remoteFundingPubKey, d.commitments.localCommitIndex + 1).publicNonce)
+ }
+ }).toMap
+ // If an interactive-tx session hasn't been fully signed, we also need to include the corresponding nonces.
+ val (interactiveTxCurrentCommitNonce_opt, interactiveTxNextCommitNonce): (Option[IndividualNonce], Map[TxId, IndividualNonce]) = d match {
+ case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
+ case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] =>
+ val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce)
+ (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce)
+ case _ => (None, Map.empty)
+ }
+ case d: DATA_NORMAL => d.spliceStatus match {
+ case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] =>
+ val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce)
+ (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce)
+ case _ => (None, Map.empty)
+ }
+ case _ => (None, Map.empty)
+ }
+ val nonceTlvs = Set(
+ interactiveTxCurrentCommitNonce_opt.map(nonce => ChannelReestablishTlv.CurrentCommitNonceTlv(nonce)),
+ if (nextCommitNonces.nonEmpty || interactiveTxNextCommitNonce.nonEmpty) Some(ChannelReestablishTlv.NextLocalNoncesTlv(nextCommitNonces.toSeq ++ interactiveTxNextCommitNonce.toSeq)) else None
+ ).flatten
+
val channelReestablish = ChannelReestablish(
channelId = d.channelId,
nextLocalCommitmentNumber = nextLocalCommitmentNumber,
nextRemoteRevocationNumber = d.commitments.remoteCommitIndex,
yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
- tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs)
+ tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs ++ nonceTlvs)
)
// we update local/remote connection-local global/local features, we don't persist it right now
val d1 = Helpers.updateFeatures(d, localInit, remoteInit)
@@ -2461,78 +2539,121 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
})
when(SYNCING)(handleExceptions {
- case Event(_: ChannelReestablish, _: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
- goto(WAIT_FOR_FUNDING_CONFIRMED)
+ case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
+ Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ goto(WAIT_FOR_FUNDING_CONFIRMED)
+ }
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) =>
- channelReestablish.nextFundingTxId_opt match {
- case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 =>
- // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received
- // their commit_sig or their tx_signatures (depending on who must send tx_signatures first).
- val fundingParams = d.signingSession.fundingParams
- val commitSig = d.signingSession.remoteCommit.sign(d.channelParams, d.signingSession.remoteCommitParams, channelKeys, d.signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, d.signingSession.commitInput(channelKeys), fundingParams.commitmentFormat)
- goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig
- case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED)
+ d.signingSession.fundingParams.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat if !channelReestablish.nextCommitNonces.contains(d.signingSession.fundingTxId) =>
+ val f = MissingCommitNonce(d.channelId, d.signingSession.fundingTxId, commitmentNumber = 1)
+ handleLocalError(f, d, Some(channelReestablish))
+ case _ =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ channelReestablish.nextFundingTxId_opt match {
+ case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 =>
+ // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received
+ // their commit_sig or their tx_signatures (depending on who must send tx_signatures first).
+ val fundingParams = d.signingSession.fundingParams
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ d.signingSession.remoteCommit.sign(d.channelParams, d.signingSession.remoteCommitParams, channelKeys, d.signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, d.signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match {
+ case Left(e) => handleLocalError(e, d, Some(channelReestablish))
+ case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig
+ }
+ case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED)
+ }
}
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
- channelReestablish.nextFundingTxId_opt match {
- case Some(fundingTxId) =>
- d.status match {
- case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
- if (channelReestablish.nextLocalCommitmentNumber == 0) {
- // They haven't received our commit_sig: we retransmit it.
- // We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
- val fundingParams = signingSession.fundingParams
- val commitSig = signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat)
- goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig
- } else {
- // They have already received our commit_sig, but we were waiting for them to send either commit_sig or
- // tx_signatures first. We wait for their message before sending our tx_signatures.
- goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED)
- }
- case _ if d.latestFundingTx.sharedTx.txId == fundingTxId =>
- // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
- // and our commit_sig if they haven't received it already.
- if (channelReestablish.nextLocalCommitmentNumber == 0) {
- val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat)
- goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs)
- } else {
- goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs
+ val pendingRbf_opt = d.status match {
+ // Note that we only consider RBF attempts that are also pending for our peer: otherwise it means we have
+ // disconnected before they sent their commit_sig, in which case they will abort the RBF attempt on reconnection.
+ case DualFundingStatus.RbfWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession)
+ case _ => None
+ }
+ Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, pendingRbf_opt) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ channelReestablish.nextFundingTxId_opt match {
+ case Some(fundingTxId) =>
+ d.status match {
+ case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
+ if (channelReestablish.nextLocalCommitmentNumber == 0) {
+ // They haven't received our commit_sig: we retransmit it.
+ // We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
+ val fundingParams = signingSession.fundingParams
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match {
+ case Left(e) => handleLocalError(e, d, Some(channelReestablish))
+ case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig
+ }
+ } else {
+ // They have already received our commit_sig, but we were waiting for them to send either commit_sig or
+ // tx_signatures first. We wait for their message before sending our tx_signatures.
+ goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED)
+ }
+ case _ if d.latestFundingTx.sharedTx.txId == fundingTxId =>
+ // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
+ // and our commit_sig if they haven't received it already.
+ if (channelReestablish.nextLocalCommitmentNumber == 0) {
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
+ case Left(e) => handleLocalError(e, d, Some(channelReestablish))
+ case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs)
+ }
+ } else {
+ goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs
+ }
+ case _ =>
+ // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving
+ // their tx_complete): we tell them to abort that RBF attempt.
+ goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage)
}
- case _ =>
- // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving
- // their tx_complete): we tell them to abort that RBF attempt.
- goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage)
+ case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
- case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
- case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) =>
- log.debug("re-sending channel_ready")
- val channelReady = createChannelReady(d.aliases, d.commitments.channelParams)
- goto(WAIT_FOR_CHANNEL_READY) sending channelReady
+ case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) =>
+ Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ val channelReady = createChannelReady(d.aliases, d.commitments)
+ goto(WAIT_FOR_CHANNEL_READY) sending channelReady
+ }
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) =>
- log.debug("re-sending channel_ready")
- val channelReady = createChannelReady(d.aliases, d.commitments.channelParams)
- // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
- // and our commit_sig if they haven't received it already.
- channelReestablish.nextFundingTxId_opt match {
- case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId =>
- d.commitments.latest.localFundingStatus.localSigs_opt match {
- case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 =>
- log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
- val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat)
- goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady)
- case Some(txSigs) =>
- log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
- goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady)
- case None =>
- log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus)
- goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
+ Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ val channelReady = createChannelReady(d.aliases, d.commitments)
+ // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
+ // and our commit_sig if they haven't received it already.
+ channelReestablish.nextFundingTxId_opt match {
+ case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId =>
+ d.commitments.latest.localFundingStatus.localSigs_opt match {
+ case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 =>
+ log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
+ case Left(e) => handleLocalError(e, d, Some(channelReestablish))
+ case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady)
+ }
+ case Some(txSigs) =>
+ log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
+ goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady)
+ case None =>
+ log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus)
+ goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
+ }
+ case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
}
- case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
}
case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
@@ -2540,164 +2661,90 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case syncFailure: SyncResult.Failure =>
handleSyncFailure(channelReestablish, syncFailure, d)
case syncSuccess: SyncResult.Success =>
- var sendQueue = Queue.empty[LightningMessage]
// normal case, our data is up-to-date
-
- // re-send channel_ready and announcement_signatures if necessary
- d.commitments.lastLocalLocked_opt match {
- case None => ()
- // We only send channel_ready for initial funding transactions.
- case Some(c) if c.fundingTxIndex != 0 => ()
- case Some(c) =>
- val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)
- // If our peer has not received our channel_ready, we retransmit it.
- val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty
- // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node
- // MUST retransmit channel_ready, otherwise it MUST NOT
- val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
- // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and
- // will also send announcement_signatures.
- val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty
- if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) {
- log.debug("re-sending channel_ready")
- val nextPerCommitmentPoint = channelKeys.commitmentPoint(1)
- sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint)
- }
- if (notAnnouncedYet) {
- // The funding transaction is confirmed, so we've already sent our announcement_signatures.
- // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures.
- // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel.
- val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex))
- localAnnSigs.foreach(annSigs => {
- announcementSigsSent += annSigs.shortChannelId
- sendQueue = sendQueue :+ annSigs
- })
- }
- }
-
- // resume splice signing session if any
- val spliceStatus1 = channelReestablish.nextFundingTxId_opt match {
- case Some(fundingTxId) =>
- d.spliceStatus match {
- case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
- if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
- // They haven't received our commit_sig: we retransmit it.
- // We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
- log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId)
- val fundingParams = signingSession.fundingParams
- val commitSig = signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat)
- sendQueue = sendQueue :+ commitSig
- }
- d.spliceStatus
- case _ if d.commitments.latest.fundingTxId == fundingTxId =>
- d.commitments.latest.localFundingStatus match {
- case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx =>
- // We've already received their commit_sig and sent our tx_signatures. We retransmit our
- // tx_signatures and our commit_sig if they haven't received it already.
- if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
- log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
- val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat)
- sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs
- } else {
- log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
- sendQueue = sendQueue :+ dfu.sharedTx.localSigs
- }
- case fundingStatus =>
- // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above.
- log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
- sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq
- }
- d.spliceStatus
- case _ =>
- // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving
- // their tx_complete): we tell them to abort that splice attempt.
- log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId")
- sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage)
- SpliceStatus.SpliceAborted
- }
- case None => d.spliceStatus
- }
+ var sendQueue = Queue.empty[LightningMessage]
+ // We re-send channel_ready and announcement_signatures for the initial funding transaction if necessary.
+ val (channelReady_opt, announcementSigs_opt) = resendChannelReadyIfNeeded(channelReestablish, d)
+ sendQueue = sendQueue ++ channelReady_opt.toSeq ++ announcementSigs_opt.toSeq
+ // If we disconnected in the middle of a signing a splice transaction, we re-send our signatures or abort.
+ val (spliceStatus1, spliceMessages) = resumeSpliceSigningSessionIfNeeded(channelReestablish, d)
+ sendQueue = sendQueue ++ spliceMessages
// Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
// transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed
// while disconnected.
- val commitments1: Commitments = channelReestablish.myCurrentFundingLocked_opt
+ val commitments1 = channelReestablish.myCurrentFundingLocked_opt
.flatMap(remoteFundingTxLocked => d.commitments.updateRemoteFundingStatus(remoteFundingTxLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1))
.getOrElse(d.commitments)
// We then clean up unsigned updates that haven't been received before the disconnection.
.discardUnsignedUpdates()
- commitments1.lastLocalLocked_opt match {
- case None => ()
- // We only send splice_locked for splice transactions.
- case Some(c) if c.fundingTxIndex == 0 => ()
- case Some(c) =>
- // If our peer has not received our splice_locked, we retransmit it.
- val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
- // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
- // will exchange announcement_signatures afterwards.
- val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))
- if (notReceivedByRemote || notAnnouncedYet) {
- // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need
- // to retransmit here.
- log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
- spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex)
- trimSpliceLockedSentIfNeeded()
- sendQueue = sendQueue :+ SpliceLocked(d.channelId, c.fundingTxId)
- }
+ // If there is a pending splice, we need to receive nonces for the corresponding transaction if we're using taproot.
+ val pendingSplice_opt = spliceStatus1 match {
+ // Note that we only consider splices that are also pending for our peer: otherwise it means we have disconnected
+ // before they sent their commit_sig, in which case they will abort the splice attempt on reconnection.
+ case SpliceStatus.SpliceWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession)
+ case _ => None
}
+ Helpers.Syncing.checkCommitNonces(channelReestablish, commitments1, pendingSplice_opt) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ // We re-send our latest splice_locked if needed.
+ val spliceLocked_opt = resendSpliceLockedIfNeeded(channelReestablish, commitments1, d.lastAnnouncement_opt)
+ sendQueue = sendQueue ++ spliceLocked_opt.toSeq
+ // We may need to retransmit updates and/or commit_sig and/or revocation to resume the channel.
+ sendQueue = sendQueue ++ syncSuccess.retransmit
+
+ commitments1.remoteNextCommitInfo match {
+ case Left(_) =>
+ // we expect them to (re-)send the revocation immediately
+ startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
+ case _ => ()
+ }
- // we may need to retransmit updates and/or commit_sig and/or revocation
- sendQueue = sendQueue ++ syncSuccess.retransmit
-
- commitments1.remoteNextCommitInfo match {
- case Left(_) =>
- // we expect them to (re-)send the revocation immediately
- startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
- case _ => ()
- }
+ // do I have something to sign?
+ if (commitments1.changes.localHasChanges) {
+ self ! CMD_SIGN()
+ }
- // do I have something to sign?
- if (commitments1.changes.localHasChanges) {
- self ! CMD_SIGN()
- }
+ // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown.
+ d.localShutdown.foreach {
+ localShutdown =>
+ log.debug("re-sending local_shutdown")
+ sendQueue = sendQueue :+ localShutdown
+ }
- // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown.
- d.localShutdown.foreach {
- localShutdown =>
- log.debug("re-sending local_shutdown")
- sendQueue = sendQueue :+ localShutdown
- }
+ if (d.commitments.announceChannel) {
+ // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable
+ startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds)
+ } else {
+ // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time,
+ // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently
+ // refresh the channel update for private channels, since we won't broadcast it to the rest of the network.
+ self ! BroadcastChannelUpdate(Reconnected)
+ }
- if (d.commitments.announceChannel) {
- // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable
- startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds)
- } else {
- // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time,
- // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently
- // refresh the channel update for private channels, since we won't broadcast it to the rest of the network.
- self ! BroadcastChannelUpdate(Reconnected)
- }
+ // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that
+ // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so
+ // we send it (if needed) when reconnected.
+ val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
+ if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) {
+ val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate
+ val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, d.commitments.latest.capacity)
+ if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
+ self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
+ }
+ }
- // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that
- // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so
- // we send it (if needed) when reconnected.
- val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
- if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) {
- val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate
- val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, d.commitments.latest.capacity)
- if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
- self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
- }
- }
+ // We tell the peer that the channel is ready to process payments that may be queued.
+ if (!shutdownInProgress) {
+ val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min
+ peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex)
+ }
- // We tell the peer that the channel is ready to process payments that may be queued.
- if (!shutdownInProgress) {
- val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min
- peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex)
+ goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue
}
-
- goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue
}
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d)
@@ -2713,14 +2760,19 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d)
case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) =>
- Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match {
- case syncFailure: SyncResult.Failure =>
- handleSyncFailure(channelReestablish, syncFailure, d)
- case syncSuccess: SyncResult.Success =>
- val commitments1 = d.commitments.discardUnsignedUpdates()
- val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown
- // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown.
- goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue
+ Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match {
+ case Some(f) => handleLocalError(f, d, Some(channelReestablish))
+ case None =>
+ remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match {
+ case syncFailure: SyncResult.Failure =>
+ handleSyncFailure(channelReestablish, syncFailure, d)
+ case syncSuccess: SyncResult.Success =>
+ val commitments1 = d.commitments.discardUnsignedUpdates()
+ val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown
+ // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown.
+ goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue
+ }
}
case Event(_: ChannelReestablish, d: DATA_NEGOTIATING) =>
@@ -2740,7 +2792,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) =>
// We retransmit our shutdown: we may have updated our script and they may not have received it.
- val localShutdown = Shutdown(d.channelId, d.localScriptPubKey)
+ val localShutdown = createShutdown(d.commitments, d.localScriptPubKey)
goto(NEGOTIATING_SIMPLE) using d sending localShutdown
// This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send
@@ -3010,7 +3062,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
context.system.eventStream.publish(ChannelStateChanged(self, nextStateData.channelId, peer, remoteNodeId, state, nextState, commitments_opt))
}
-
if (nextState == CLOSED) {
// channel is closed, scheduling this actor for self destruction
context.system.scheduler.scheduleOnce(1 minute, self, Symbol("shutdown"))
@@ -3130,12 +3181,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
}
- /** On disconnection we clear up stashes. */
+ /** On disconnection we clear up temporary mutable state that applies to the previous connection. */
onTransition {
case _ -> OFFLINE =>
announcementSigsStash = Map.empty
announcementSigsSent = Set.empty
spliceLockedSent = Map.empty[TxId, Long]
+ remoteNextCommitNonces = Map.empty
+ localCloseeNonce_opt = None
+ remoteCloseeNonce_opt = None
+ localCloserNonces_opt = None
}
/*
@@ -3353,6 +3408,117 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
}
+ private def resendChannelReadyIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (Option[ChannelReady], Option[AnnouncementSignatures]) = {
+ d.commitments.lastLocalLocked_opt match {
+ case None => (None, None)
+ // We only send channel_ready for initial funding transactions.
+ case Some(c) if c.fundingTxIndex != 0 => (None, None)
+ case Some(c) =>
+ val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)
+ // If our peer has not received our channel_ready, we retransmit it.
+ val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty
+ // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node
+ // MUST retransmit channel_ready, otherwise it MUST NOT
+ val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
+ // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and
+ // will also send announcement_signatures.
+ val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty
+ val channelReady_opt = if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) {
+ log.debug("re-sending channel_ready")
+ Some(createChannelReady(d.aliases, d.commitments))
+ } else {
+ None
+ }
+ val announcementSigs_opt = if (notAnnouncedYet) {
+ // The funding transaction is confirmed, so we've already sent our announcement_signatures.
+ // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures.
+ // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel.
+ val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex))
+ localAnnSigs.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId)
+ localAnnSigs
+ } else {
+ None
+ }
+ (channelReady_opt, announcementSigs_opt)
+ }
+ }
+
+ private def resumeSpliceSigningSessionIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (SpliceStatus, Queue[LightningMessage]) = {
+ var sendQueue = Queue.empty[LightningMessage]
+ val spliceStatus1 = channelReestablish.nextFundingTxId_opt match {
+ case Some(fundingTxId) =>
+ d.spliceStatus match {
+ case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
+ if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
+ // They haven't received our commit_sig: we retransmit it.
+ // We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
+ log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId)
+ val fundingParams = signingSession.fundingParams
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match {
+ case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage)
+ case Right(commitSig) => sendQueue = sendQueue :+ commitSig
+ }
+ }
+ d.spliceStatus
+ case _ if d.commitments.latest.fundingTxId == fundingTxId =>
+ d.commitments.latest.localFundingStatus match {
+ case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx =>
+ // We've already received their commit_sig and sent our tx_signatures. We retransmit our
+ // tx_signatures and our commit_sig if they haven't received it already.
+ if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
+ log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
+ val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
+ d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
+ case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage)
+ case Right(commitSig) => sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs
+ }
+ } else {
+ log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
+ sendQueue = sendQueue :+ dfu.sharedTx.localSigs
+ }
+ case fundingStatus =>
+ // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above.
+ log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
+ sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq
+ }
+ d.spliceStatus
+ case _ =>
+ // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving
+ // their tx_complete): we tell them to abort that splice attempt.
+ log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId")
+ sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage)
+ SpliceStatus.SpliceAborted
+ }
+ case None => d.spliceStatus
+ }
+ (spliceStatus1, sendQueue)
+ }
+
+ private def resendSpliceLockedIfNeeded(channelReestablish: ChannelReestablish, commitments: Commitments, lastAnnouncement_opt: Option[ChannelAnnouncement]): Option[SpliceLocked] = {
+ commitments.lastLocalLocked_opt match {
+ case None => None
+ // We only send splice_locked for splice transactions.
+ case Some(c) if c.fundingTxIndex == 0 => None
+ case Some(c) =>
+ // If our peer has not received our splice_locked, we retransmit it.
+ val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
+ // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
+ // will exchange announcement_signatures afterwards.
+ val notAnnouncedYet = commitments.announceChannel && lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))
+ if (notReceivedByRemote || notAnnouncedYet) {
+ // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need
+ // to retransmit here.
+ log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
+ spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex)
+ trimSpliceLockedSentIfNeeded()
+ Some(SpliceLocked(commitments.channelId, c.fundingTxId))
+ } else {
+ None
+ }
+ }
+ }
+
/**
* Return full information about a known closing tx.
*/
@@ -3393,7 +3559,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
fundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey,
pushAmount = cmd.pushAmount,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
- requestFunding_opt = cmd.requestFunding_opt
+ requestFunding_opt = cmd.requestFunding_opt,
+ channelType_opt = cmd.channelType_opt
)
Right(spliceInit)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala
index dbc079c..3cc1fce 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala
@@ -322,7 +322,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => msg match {
case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg
- case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt) =>
+ case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) =>
+ nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) }
d.deferred.foreach(self ! _)
d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi))
liquidityPurchase_opt.collect {
@@ -691,7 +692,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
case DualFundingStatus.RbfInProgress(cmd_opt, _, remoteCommitSig_opt) =>
msg match {
case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg
- case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) =>
+ case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) =>
+ nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) }
cmd_opt.foreach(cmd => cmd.replyTo ! RES_BUMP_FUNDING_FEE(rbfIndex = d.previousFundingTxs.length, signingSession.fundingTx.txId, signingSession.fundingTx.tx.localFees.truncateToSatoshi))
remoteCommitSig_opt.foreach(self ! _)
liquidityPurchase_opt.collect {
@@ -718,7 +720,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
// We still watch the funding tx for confirmation even if we can use the zero-conf channel right away.
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None)
val shortIds = createShortIdAliases(d.channelId)
- val channelReady = createChannelReady(shortIds, d.commitments.channelParams)
+ val channelReady = createChannelReady(shortIds, d.commitments)
d.deferred.foreach(self ! _)
goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady
case Left(_) => stay()
@@ -728,7 +730,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
acceptFundingTxConfirmed(w, d) match {
case Right((commitments1, _)) =>
val shortIds = createShortIdAliases(d.channelId)
- val channelReady = createChannelReady(shortIds, d.commitments.channelParams)
+ val channelReady = createChannelReady(shortIds, d.commitments)
reportRbfFailure(d.status, InvalidRbfTxConfirmed(d.channelId))
val toSend = d.status match {
case DualFundingStatus.WaitingForConfirmations | DualFundingStatus.RbfAborted => Seq(channelReady)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala
index 23d0b5b..23651e6 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala
@@ -19,20 +19,21 @@ package fr.acinq.eclair.channel.fsm
import akka.actor.Status
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.pattern.pipe
-import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script}
+import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
+import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce}
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel._
import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId
-import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys}
+import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
import fr.acinq.eclair.io.Peer.OpenChannelResponse
-import fr.acinq.eclair.transactions.Scripts
-import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
-import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream}
+import fr.acinq.eclair.transactions.Transactions
+import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, TlvStream}
import fr.acinq.eclair.{MilliSatoshiLong, randomKey, toLongId}
import scodec.bits.ByteVector
@@ -72,10 +73,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions {
case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) =>
- val fundingPubKey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey
+ val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
val localShutdownScript = input.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
+ val localNonce = input.channelType.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => None
+ }
val open = OpenChannel(
chainHash = nodeParams.chainHash,
temporaryChannelId = input.temporaryChannelId,
@@ -88,7 +93,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
feeratePerKw = input.commitTxFeerate,
toSelfDelay = input.proposedCommitParams.toRemoteDelay,
maxAcceptedHtlcs = input.proposedCommitParams.localMaxAcceptedHtlcs,
- fundingPubkey = fundingPubKey,
+ fundingPubkey = fundingKey.publicKey,
revocationBasepoint = channelKeys.revocationBasePoint,
paymentBasepoint = input.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint),
delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint,
@@ -96,8 +101,11 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
firstPerCommitmentPoint = channelKeys.commitmentPoint(0),
channelFlags = input.channelFlags,
tlvStream = TlvStream(
- ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
- ChannelTlv.ChannelTypeTlv(input.channelType)
+ Set(
+ Some(ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript)),
+ Some(ChannelTlv.ChannelTypeTlv(input.channelType)),
+ localNonce.map(n => ChannelTlv.NextLocalNonceTlv(n))
+ ).flatten[OpenChannelTlv]
))
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open
})
@@ -117,13 +125,17 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
htlcBasepoint = open.htlcBasepoint,
initFeatures = d.initFundee.remoteInit.features,
upfrontShutdownScript_opt = remoteShutdownScript)
- val fundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey
+ val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
val channelParams = ChannelParams(d.initFundee.temporaryChannelId, d.initFundee.channelConfig, channelFeatures, d.initFundee.localChannelParams, remoteChannelParams, open.channelFlags)
val localCommitParams = CommitParams(d.initFundee.proposedCommitParams.localDustLimit, d.initFundee.proposedCommitParams.localHtlcMinimum, d.initFundee.proposedCommitParams.localMaxHtlcValueInFlight, d.initFundee.proposedCommitParams.localMaxAcceptedHtlcs, open.toSelfDelay)
val remoteCommitParams = CommitParams(open.dustLimitSatoshis, open.htlcMinimumMsat, open.maxHtlcValueInFlightMsat, open.maxAcceptedHtlcs, d.initFundee.proposedCommitParams.toRemoteDelay)
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used.
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
val localShutdownScript = d.initFundee.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
+ val localNonce = d.initFundee.channelType.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => None
+ }
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
dustLimitSatoshis = localCommitParams.dustLimit,
maxHtlcValueInFlightMsat = localCommitParams.maxHtlcValueInFlight,
@@ -132,16 +144,18 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
htlcMinimumMsat = localCommitParams.htlcMinimum,
toSelfDelay = remoteCommitParams.toSelfDelay,
maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs,
- fundingPubkey = fundingPubkey,
+ fundingPubkey = fundingKey.publicKey,
revocationBasepoint = channelKeys.revocationBasePoint,
paymentBasepoint = d.initFundee.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint),
delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint,
htlcBasepoint = channelKeys.htlcBasePoint,
firstPerCommitmentPoint = channelKeys.commitmentPoint(0),
- tlvStream = TlvStream(
- ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
- ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)
- ))
+ tlvStream = TlvStream(Set(
+ Some(ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript)),
+ Some(ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)),
+ localNonce.map(n => ChannelTlv.NextLocalNonceTlv(n))
+ ).flatten[AcceptChannelTlv]))
+ remoteNextCommitNonces = open.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap
goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, d.initFundee.channelType, localCommitParams, remoteCommitParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept
}
@@ -170,11 +184,12 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
upfrontShutdownScript_opt = remoteShutdownScript)
log.info("remote will use fundingMinDepth={}", accept.minimumDepth)
val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
- val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingKey.publicKey, accept.fundingPubkey)))
+ val fundingPubkeyScript = Transactions.makeFundingScript(localFundingKey.publicKey, accept.fundingPubkey, d.initFunder.channelType.commitmentFormat).pubkeyScript
wallet.makeFundingTx(fundingPubkeyScript, d.initFunder.fundingAmount, d.initFunder.fundingTxFeerate, d.initFunder.fundingTxFeeBudget_opt).pipeTo(self)
val channelParams = ChannelParams(d.initFunder.temporaryChannelId, d.initFunder.channelConfig, channelFeatures, d.initFunder.localChannelParams, remoteChannelParams, d.lastSent.channelFlags)
val localCommitParams = CommitParams(d.initFunder.proposedCommitParams.localDustLimit, d.initFunder.proposedCommitParams.localHtlcMinimum, d.initFunder.proposedCommitParams.localMaxHtlcValueInFlight, d.initFunder.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay)
val remoteCommitParams = CommitParams(accept.dustLimitSatoshis, accept.htlcMinimumMsat, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.initFunder.proposedCommitParams.toRemoteDelay)
+ remoteNextCommitNonces = accept.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, d.initFunder.channelType, localCommitParams, remoteCommitParams, d.initFunder.fundingAmount, d.initFunder.pushAmount_opt.getOrElse(0 msat), d.initFunder.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo)
}
@@ -205,26 +220,33 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = d.fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = d.pushAmount, remotePushAmount = 0 msat, d.commitTxFeerate, d.commitmentFormat, fundingTx.txid, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match {
case Left(ex) => handleLocalError(ex, d, None)
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) =>
- require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
+ require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!")
+ val remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint)
val localSigOfRemoteTx = d.commitmentFormat match {
- case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0)
+ remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match {
+ case Some(remoteNonce) =>
+ remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
+ case Left(_) => Left(InvalidCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
+ case Right(psig) => Right(psig)
+ }
+ case None => Left(MissingCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
+ }
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
+ }
+ localSigOfRemoteTx match {
+ case Left(f) => handleLocalError(f, d, None)
+ case Right(localSig) =>
+ val fundingCreated = FundingCreated(temporaryChannelId, fundingTx.txid, fundingTxOutputIndex, localSig)
+ val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex)
+ val channelParams1 = d.channelParams.copy(channelId = channelId)
+ peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
+ txPublisher ! SetChannelId(remoteNodeId, channelId)
+ context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
+ // NB: we don't send a ChannelSignatureSent for the first commit
+ goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated
}
- val remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint)
- // signature of their initial commitment tx that pays remote pushMsat
- val fundingCreated = FundingCreated(
- temporaryChannelId = temporaryChannelId,
- fundingTxId = fundingTx.txid,
- fundingOutputIndex = fundingTxOutputIndex,
- signature = localSigOfRemoteTx
- )
- val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex)
- val channelParams1 = d.channelParams.copy(channelId = channelId)
- peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
- txPublisher ! SetChannelId(remoteNodeId, channelId)
- context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
- // NB: we don't send a ChannelSignatureSent for the first commit
- goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated
}
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
@@ -250,57 +272,73 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
})
when(WAIT_FOR_FUNDING_CREATED)(handleExceptions {
- case Event(FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), d: DATA_WAIT_FOR_FUNDING_CREATED) =>
+ case Event(fc@FundingCreated(_, fundingTxId, fundingTxOutputIndex, _, _), d: DATA_WAIT_FOR_FUNDING_CREATED) =>
val temporaryChannelId = d.channelParams.channelId
val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0, d.commitmentFormat)
val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint, d.commitmentFormat)
- // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat)
Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = 0 sat, remoteFundingAmount = d.fundingAmount, localPushAmount = 0 msat, remotePushAmount = d.pushAmount, d.commitTxFeerate, d.commitmentFormat, fundingTxId, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match {
- case Left(ex) => handleLocalError(ex, d, None)
+ case Left(ex) => handleLocalError(ex, d, Some(fc))
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) =>
// check remote signature validity
- localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(remoteSig)) match {
- case false => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, commitmentNumber = 0, localCommitTx.tx), d, None)
+ val isRemoteSigValid = fc.sigOrPartialSig match {
+ case psig: PartialSignatureWithNonce =>
+ val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0)
+ localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce)
+ case sig: IndividualSignature =>
+ localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig)
+ }
+ isRemoteSigValid match {
+ case false => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, commitmentNumber = 0, localCommitTx.tx), d, Some(fc))
case true =>
+ val channelId = toLongId(fundingTxId, fundingTxOutputIndex)
val localSigOfRemoteTx = d.commitmentFormat match {
- case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0)
+ remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match {
+ case Some(remoteNonce) =>
+ remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
+ case Left(_) => Left(InvalidCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
+ case Right(psig) => Right(psig)
+ }
+ case None => Left(MissingCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
+ }
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
+ }
+ localSigOfRemoteTx match {
+ case Left(f) => handleLocalError(f, d, Some(fc))
+ case Right(localSig) =>
+ val fundingSigned = FundingSigned(channelId, localSig)
+ val commitment = Commitment(
+ fundingTxIndex = 0,
+ firstRemoteCommitIndex = 0,
+ fundingInput = localCommitTx.input.outPoint,
+ fundingAmount = localCommitTx.input.txOut.amount,
+ remoteFundingPubKey = d.remoteFundingPubKey,
+ localFundingStatus = SingleFundedUnconfirmedFundingTx(None),
+ remoteFundingStatus = RemoteFundingStatus.NotLocked,
+ commitmentFormat = d.commitmentFormat,
+ localCommitParams = d.localCommitParams,
+ localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, fc.sigOrPartialSig, htlcRemoteSigs = Nil),
+ remoteCommitParams = d.remoteCommitParams,
+ remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint),
+ nextRemoteCommit_opt = None)
+ val commitments = Commitments(
+ channelParams = d.channelParams.copy(channelId = channelId),
+ changes = CommitmentChanges.init(),
+ active = List(commitment),
+ remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
+ remotePerCommitmentSecrets = ShaChain.init,
+ originChannels = Map.empty)
+ peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
+ txPublisher ! SetChannelId(remoteNodeId, channelId)
+ context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
+ context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
+ // NB: we don't send a ChannelSignatureSent for the first commit
+ log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId)
+ watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
+ goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned
}
- val channelId = toLongId(fundingTxId, fundingTxOutputIndex)
- val fundingSigned = FundingSigned(
- channelId = channelId,
- signature = localSigOfRemoteTx
- )
- val commitment = Commitment(
- fundingTxIndex = 0,
- firstRemoteCommitIndex = 0,
- fundingInput = localCommitTx.input.outPoint,
- fundingAmount = localCommitTx.input.txOut.amount,
- remoteFundingPubKey = d.remoteFundingPubKey,
- localFundingStatus = SingleFundedUnconfirmedFundingTx(None),
- remoteFundingStatus = RemoteFundingStatus.NotLocked,
- commitmentFormat = d.commitmentFormat,
- localCommitParams = d.localCommitParams,
- localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil),
- remoteCommitParams = d.remoteCommitParams,
- remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint),
- nextRemoteCommit_opt = None)
- val commitments = Commitments(
- channelParams = d.channelParams.copy(channelId = channelId),
- changes = CommitmentChanges.init(),
- active = List(commitment),
- remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
- remotePerCommitmentSecrets = ShaChain.init,
- originChannels = Map.empty)
- peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
- txPublisher ! SetChannelId(remoteNodeId, channelId)
- context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
- context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
- // NB: we don't send a ChannelSignatureSent for the first commit
- log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId)
- watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
- goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned
}
}
@@ -312,15 +350,22 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
})
when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions {
- case Event(msg@FundingSigned(_, remoteSig, _), d: DATA_WAIT_FOR_FUNDING_SIGNED) =>
+ case Event(fundingSigned: FundingSigned, d: DATA_WAIT_FOR_FUNDING_SIGNED) =>
// we make sure that their sig checks out and that our first commit tx is spendable
- val fundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey
- d.localCommitTx.checkRemoteSig(fundingPubkey, d.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(remoteSig)) match {
+ val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
+ val isRemoteSigValid = fundingSigned.sigOrPartialSig match {
+ case psig: PartialSignatureWithNonce =>
+ val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0)
+ d.localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce)
+ case sig: IndividualSignature =>
+ d.localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig)
+ }
+ isRemoteSigValid match {
case false =>
// we rollback the funding tx, it will never be published
wallet.rollback(d.fundingTx)
d.replyTo ! OpenChannelResponse.Rejected("invalid commit signatures")
- handleLocalError(InvalidCommitmentSignature(d.channelId, d.fundingTx.txid, commitmentNumber = 0, d.localCommitTx.tx), d, Some(msg))
+ handleLocalError(InvalidCommitmentSignature(d.channelId, d.fundingTx.txid, commitmentNumber = 0, d.localCommitTx.tx), d, Some(fundingSigned))
case true =>
val commitment = Commitment(
fundingTxIndex = 0,
@@ -332,7 +377,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
remoteFundingStatus = RemoteFundingStatus.NotLocked,
commitmentFormat = d.commitmentFormat,
localCommitParams = d.localCommitParams,
- localCommit = LocalCommit(0, d.localSpec, d.localCommitTx.tx.txid, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil),
+ localCommit = LocalCommit(0, d.localSpec, d.localCommitTx.tx.txid, fundingSigned.sigOrPartialSig, htlcRemoteSigs = Nil),
remoteCommitParams = d.remoteCommitParams,
remoteCommit = d.remoteCommit,
nextRemoteCommit_opt = None
@@ -403,7 +448,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
// We still watch the funding tx for confirmation even if we can use the zero-conf channel right away.
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None)
val shortIds = createShortIdAliases(d.channelId)
- val channelReady = createChannelReady(shortIds, d.commitments.channelParams)
+ val channelReady = createChannelReady(shortIds, d.commitments)
d.deferred.foreach(self ! _)
goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady
case Left(_) => stay()
@@ -413,7 +458,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
acceptFundingTxConfirmed(w, d) match {
case Right((commitments1, _)) =>
val shortIds = createShortIdAliases(d.channelId)
- val channelReady = createChannelReady(shortIds, d.commitments.channelParams)
+ val channelReady = createChannelReady(shortIds, d.commitments)
d.deferred.foreach(self ! _)
goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady
case Left(_) => stay()
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala
index 531e5b7..eea86d6 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala
@@ -24,8 +24,10 @@ import fr.acinq.eclair.channel.Helpers.getRelayFees
import fr.acinq.eclair.channel.LocalFundingStatus.{ConfirmedFundingTx, DualFundedUnconfirmedFundingTx, SingleFundedUnconfirmedFundingTx}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL}
+import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.db.RevokedHtlcInfoCleaner
-import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReady, ChannelReadyTlv, TlvStream}
+import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{RealShortChannelId, ShortChannelId}
import scala.concurrent.duration.{DurationInt, FiniteDuration}
@@ -121,10 +123,18 @@ trait CommonFundingHandlers extends CommonHandlers {
aliases
}
- def createChannelReady(aliases: ShortIdAliases, params: ChannelParams): ChannelReady = {
+ def createChannelReady(aliases: ShortIdAliases, commitments: Commitments): ChannelReady = {
+ val params = commitments.channelParams
val nextPerCommitmentPoint = channelKeys.commitmentPoint(1)
- // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway
- ChannelReady(params.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias)))
+ // Note that we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway.
+ commitments.latest.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
+ val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, commitments.latest.remoteFundingPubKey, 1)
+ ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias, nextLocalNonce.publicNonce)
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat =>
+ ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias)
+ }
}
def receiveChannelReady(aliases: ShortIdAliases, channelReady: ChannelReady, commitments: Commitments): DATA_NORMAL = {
@@ -148,6 +158,7 @@ trait CommonFundingHandlers extends CommonHandlers {
},
remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)
)
+ channelReady.nextCommitNonce_opt.foreach(nonce => remoteNextCommitNonces = remoteNextCommitNonces + (commitments.latest.fundingTxId -> nonce))
peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0)
DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, SpliceStatus.NoSplice, None, None, None)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
index bc41016..0833816 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
@@ -21,8 +21,10 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.Features
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
import fr.acinq.eclair.channel._
+import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.io.Peer
+import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol.{ClosingComplete, HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage}
import scodec.bits.ByteVector
@@ -132,17 +134,31 @@ trait CommonHandlers {
finalScriptPubkey
}
+ def createShutdown(commitments: Commitments, finalScriptPubKey: ByteVector): Shutdown = {
+ commitments.latest.commitmentFormat match {
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ // We create a fresh local closee nonce every time we send shutdown.
+ val localFundingPubKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey
+ val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitments.latest.remoteFundingPubKey, commitments.latest.fundingTxId)
+ localCloseeNonce_opt = Some(localCloseeNonce)
+ Shutdown(commitments.channelId, finalScriptPubKey, localCloseeNonce.publicNonce)
+ case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat =>
+ Shutdown(commitments.channelId, finalScriptPubKey)
+ }
+ }
+
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = {
val localScript = localShutdown.scriptPubKey
val remoteScript = remoteShutdown.scriptPubKey
val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
- MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, commitments.latest, localScript, remoteScript, closingFeerate) match {
+ MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, commitments.latest, localScript, remoteScript, closingFeerate, remoteShutdown.closeeNonce_opt) match {
case Left(f) =>
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, Nil, Nil)
(d, None)
- case Right((closingTxs, closingComplete)) =>
+ case Right((closingTxs, closingComplete, closerNonces)) =>
log.debug("signing local mutual close transactions: {}", closingTxs)
+ localCloserNonces_opt = Some(closerNonces)
val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, closingTxs :: Nil, Nil)
(d, Some(closingComplete))
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala
index 261e1ca..90e2220 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala
@@ -196,7 +196,7 @@ trait ErrorHandlers extends CommonHandlers {
}
}
- def spendLocalCurrent(d: ChannelDataWithCommitments) = {
+ def spendLocalCurrent(d: ChannelDataWithCommitments): FSM.State[ChannelState, ChannelData] = {
val outdatedCommitment = d match {
case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true
case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
index ec33147..ff52d0b 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
@@ -22,19 +22,22 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ScriptFlags
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.psbt.Psbt
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut}
import fr.acinq.eclair.blockchain.OnChainChannelFunder
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
+import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose
import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit
+import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
-import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64}
@@ -92,7 +95,7 @@ object InteractiveTxBuilder {
sealed trait Response
case class SendMessage(sessionId: ByteVector32, msg: LightningMessage) extends Response
- case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase]) extends Response
+ case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase], nextRemoteCommitNonce_opt: Option[(TxId, IndividualNonce)]) extends Response
sealed trait Failed extends Response { def cause: ChannelException }
case class LocalFailure(cause: ChannelException) extends Failed
case class RemoteFailure(cause: ChannelException) extends Failed
@@ -104,9 +107,19 @@ object InteractiveTxBuilder {
case class SharedFundingInput(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitmentFormat: CommitmentFormat) {
val weight: Int = commitmentFormat.fundingInputWeight
- def sign(channelKeys: ChannelKeys, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ChannelSpendSignature.IndividualSignature = {
+ def sign(channelId: ByteVector32, channelKeys: ChannelKeys, tx: Transaction, localNonce_opt: Option[LocalNonce], remoteNonce_opt: Option[IndividualNonce], spentUtxos: Map[OutPoint, TxOut]): Either[ChannelException, ChannelSpendSignature] = {
val localFundingKey = channelKeys.fundingKey(fundingTxIndex)
- Transactions.SpliceTx(info, tx).sign(localFundingKey, remoteFundingPubkey, spentUtxos)
+ val spliceTx = Transactions.SpliceTx(info, tx)
+ commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => Right(spliceTx.sign(localFundingKey, remoteFundingPubkey, spentUtxos))
+ case _: SimpleTaprootChannelCommitmentFormat => (localNonce_opt, remoteNonce_opt) match {
+ case (Some(localNonce), Some(remoteNonce)) => spliceTx.partialSign(localFundingKey, remoteFundingPubkey, spentUtxos, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match {
+ case Left(_) => Left(InvalidFundingNonce(channelId, tx.txid))
+ case Right(sig) => Right(sig)
+ }
+ case _ => Left(MissingFundingNonce(channelId, tx.txid))
+ }
+ }
}
}
@@ -311,11 +324,11 @@ object InteractiveTxBuilder {
remoteInputs: Seq[IncomingInput] = Nil,
localOutputs: Seq[OutgoingOutput] = Nil,
remoteOutputs: Seq[IncomingOutput] = Nil,
- txCompleteSent: Boolean = false,
- txCompleteReceived: Boolean = false,
+ txCompleteSent: Option[TxComplete] = None,
+ txCompleteReceived: Option[TxComplete] = None,
inputsReceivedCount: Int = 0,
outputsReceivedCount: Int = 0) {
- val isComplete: Boolean = txCompleteSent && txCompleteReceived
+ val isComplete: Boolean = txCompleteSent.isDefined && txCompleteReceived.isDefined
}
/** Unsigned transaction created collaboratively. */
@@ -331,6 +344,8 @@ object InteractiveTxBuilder {
val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut
// Note that the truncation is a no-op: sub-satoshi balances are carried over from inputs to outputs and cancel out.
val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi
+ // Outputs spent by this transaction, in the order in which they appear in the transaction inputs.
+ val spentOutputs: Seq[TxOut] = (sharedInput_opt.toSeq ++ localInputs ++ remoteInputs).sortBy(_.serialId).map(_.txOut)
// When signing transactions that include taproot inputs, we must provide details about all of the transaction's inputs.
val inputDetails: Map[OutPoint, TxOut] = (sharedInput_opt.toSeq.map(i => i.outPoint -> i.txOut) ++ localInputs.map(i => i.outPoint -> i.txOut) ++ remoteInputs.map(i => i.outPoint -> i.txOut)).toMap
@@ -457,13 +472,20 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
private val log = context.log
private val localFundingKey: PrivateKey = channelKeys.fundingKey(purpose.fundingTxIndex)
- private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingKey.publicKey, fundingParams.remoteFundingPubKey)))
+ private val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript
private val remoteNodeId = channelParams.remoteParams.nodeId
private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match {
case rbf: FundingTxRbf => rbf.previousTransactions
case rbf: SpliceTxRbf => rbf.previousTransactions
case _ => Nil
}
+ // Nonce we will use to sign the shared input, if we are splicing a taproot channel.
+ private val localFundingNonce_opt: Option[LocalNonce] = fundingParams.sharedInput_opt.flatMap(sharedInput => sharedInput.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => None
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val previousFundingKey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey
+ Some(NonceGenerator.signingNonce(previousFundingKey, sharedInput.remoteFundingPubkey, sharedInput.info.outPoint.txid))
+ })
def start(): Behavior[Command] = {
val txFunder = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, fundingPubkeyScript, purpose, wallet))
@@ -518,16 +540,39 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
case i: Input.Shared => TxAddInput(fundingParams.channelId, i.serialId, i.outPoint, i.sequence)
}
replyTo ! SendMessage(sessionId, message)
- val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = false)
+ val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = None)
receive(next)
case (addOutput: Output) +: tail =>
val message = TxAddOutput(fundingParams.channelId, addOutput.serialId, addOutput.amount, addOutput.pubkeyScript)
replyTo ! SendMessage(sessionId, message)
- val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = false)
+ val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = None)
receive(next)
case Nil =>
- replyTo ! SendMessage(sessionId, TxComplete(fundingParams.channelId))
- val next = session.copy(txCompleteSent = true)
+ val txComplete = fundingParams.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => TxComplete(fundingParams.channelId)
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ // We don't have more inputs or outputs to contribute to the shared transaction.
+ // If our peer doesn't have anything more to contribute either, we will proceed to exchange commitment
+ // signatures spending this shared transaction, so we need to provide nonces to create those signatures.
+ // If our peer adds more inputs or outputs, we will simply send a new tx_complete message in response with
+ // nonces for the updated shared transaction.
+ // Note that we don't validate the shared transaction at that point: this will be done later once we've
+ // both sent tx_complete. If the shared transaction is invalid, we will abort and discard our nonces.
+ val fundingTxId = Transaction(
+ version = 2,
+ txIn = (session.localInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence)) ++ session.remoteInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence))).sortBy(_._1).map(_._2),
+ txOut = (session.localOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript)) ++ session.remoteOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript))).sortBy(_._1).map(_._2),
+ lockTime = fundingParams.lockTime
+ ).txid
+ TxComplete(
+ channelId = fundingParams.channelId,
+ commitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex).publicNonce,
+ nextCommitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex + 1).publicNonce,
+ fundingNonce_opt = localFundingNonce_opt.map(_.publicNonce),
+ )
+ }
+ replyTo ! SendMessage(sessionId, txComplete)
+ val next = session.copy(txCompleteSent = Some(txComplete))
if (next.isComplete) {
validateAndSign(next)
} else {
@@ -603,7 +648,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
val next = session.copy(
remoteInputs = session.remoteInputs :+ input,
inputsReceivedCount = session.inputsReceivedCount + 1,
- txCompleteReceived = false,
+ txCompleteReceived = None,
)
send(next)
}
@@ -616,7 +661,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
val next = session.copy(
remoteOutputs = session.remoteOutputs :+ output,
outputsReceivedCount = session.outputsReceivedCount + 1,
- txCompleteReceived = false,
+ txCompleteReceived = None,
)
send(next)
}
@@ -625,7 +670,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
case Some(_) =>
val next = session.copy(
remoteInputs = session.remoteInputs.filterNot(_.serialId == removeInput.serialId),
- txCompleteReceived = false,
+ txCompleteReceived = None,
)
send(next)
case None =>
@@ -637,15 +682,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
case Some(_) =>
val next = session.copy(
remoteOutputs = session.remoteOutputs.filterNot(_.serialId == removeOutput.serialId),
- txCompleteReceived = false,
+ txCompleteReceived = None,
)
send(next)
case None =>
replyTo ! RemoteFailure(UnknownSerialId(fundingParams.channelId, removeOutput.serialId))
unlockAndStop(session)
}
- case _: TxComplete =>
- val next = session.copy(txCompleteReceived = true)
+ case txComplete: TxComplete =>
+ val next = session.copy(txCompleteReceived = Some(txComplete))
if (next.isComplete) {
validateAndSign(next)
} else {
@@ -675,7 +720,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
replyTo ! RemoteFailure(cause)
unlockAndStop(session)
case Right(completeTx) =>
- signCommitTx(completeTx)
+ signCommitTx(completeTx, session.txCompleteReceived.flatMap(_.nonces_opt))
}
case _: WalletFailure =>
replyTo ! RemoteFailure(UnconfirmedInteractiveTxInputs(fundingParams.channelId))
@@ -731,7 +776,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId))
}
- val sharedInput_opt = fundingParams.sharedInput_opt.map(_ => {
+ val sharedInput_opt = fundingParams.sharedInput_opt.map(sharedInput => {
if (fundingParams.remoteContribution >= 0.sat) {
// If remote has a positive contribution, we do not check their post-splice reserve level, because they are improving
// their situation, even if they stay below the requirement. Note that if local splices-in some funds in the same
@@ -748,6 +793,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
log.warn("invalid interactive tx: shared input included multiple times")
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId))
}
+ sharedInput.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => ()
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ // If we're spending a taproot channel, our peer must provide a nonce for the shared input.
+ val remoteFundingNonce_opt: Option[IndividualNonce] = session.txCompleteReceived.flatMap(_.nonces_opt).flatMap(_.fundingNonce_opt)
+ if (remoteFundingNonce_opt.isEmpty) return Left(MissingFundingNonce(fundingParams.channelId, sharedInput.info.outPoint.txid))
+ }
sharedInputs.headOption match {
case Some(input) => input
case None =>
@@ -763,6 +815,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId))
}
+ // If we're using taproot, our peer must provide commit nonces for the funding transaction.
+ fundingParams.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => ()
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ val remoteCommitNonces_opt = session.txCompleteReceived.flatMap(_.nonces_opt)
+ if (remoteCommitNonces_opt.isEmpty) return Left(MissingCommitNonce(fundingParams.channelId, tx.txid, purpose.remoteCommitIndex))
+ }
+
// The transaction isn't signed yet, and segwit witnesses can be arbitrarily low (e.g. when using an OP_1 script),
// so we use empty witnesses to provide a lower bound on the transaction weight.
if (tx.weight() > Transactions.MAX_STANDARD_TX_WEIGHT) {
@@ -828,7 +888,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
Right(sharedTx)
}
- private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = {
+ private def signCommitTx(completeTx: SharedTransaction, remoteNonces_opt: Option[TxCompleteTlv.Nonces]): Behavior[Command] = {
val fundingTx = completeTx.buildUnsignedTx()
val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript)
val liquidityFee = fundingParams.liquidityFees(liquidityPurchase_opt)
@@ -851,22 +911,35 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
unlockAndStop(completeTx)
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) =>
require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!")
- fundingParams.commitmentFormat match {
- case _: SegwitV0CommitmentFormat =>
- val localSigOfRemoteTx = remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig
+ val localSigOfRemoteTx = fundingParams.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey))
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ remoteNonces_opt match {
+ case Some(remoteNonces) =>
+ val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingTx.txid)
+ remoteCommitTx.partialSign(localFundingKey, fundingParams.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonces.commitNonce)) match {
+ case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, fundingTx.txid, purpose.remoteCommitIndex))
+ case Right(localSig) => Right(localSig)
+ }
+ case None => Left(MissingCommitNonce(fundingParams.channelId, fundingTx.txid, purpose.remoteCommitIndex))
+ }
+ }
+ localSigOfRemoteTx match {
+ case Left(cause) =>
+ replyTo ! RemoteFailure(cause)
+ unlockAndStop(completeTx)
+ case Right(localSigOfRemoteTx) =>
val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList
- val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures)
+ val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, batchSize = 1)
val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid)
val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint)
- signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit)
- case _: SimpleTaprootChannelCommitmentFormat =>
- ???
+ signFundingTx(completeTx, remoteNonces_opt, localCommitSig, localCommit, remoteCommit)
}
}
}
- private def signFundingTx(completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = {
- signTx(completeTx)
+ private def signFundingTx(completeTx: SharedTransaction, remoteNonces_opt: Option[TxCompleteTlv.Nonces], commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = {
+ signTx(completeTx, remoteNonces_opt.flatMap(_.fundingNonce_opt))
Behaviors.receiveMessagePartial {
case SignTransactionResult(signedTx) =>
log.info(s"interactive-tx txid=${signedTx.txId} partially signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", signedTx.tx.localInputs.length, signedTx.tx.remoteInputs.length, signedTx.tx.localOutputs.length, signedTx.tx.remoteOutputs.length)
@@ -903,7 +976,8 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
remoteCommit,
liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator))
)
- replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt)
+ val nextRemoteCommitNonce_opt = remoteNonces_opt.map(n => signedTx.txId -> n.nextCommitNonce)
+ replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt)
Behaviors.stopped
case WalletFailure(t) =>
log.error("could not sign funding transaction: ", t)
@@ -918,53 +992,56 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
}
}
- private def signTx(unsignedTx: SharedTransaction): Unit = {
+ private def signTx(unsignedTx: SharedTransaction, remoteFundingNonce_opt: Option[IndividualNonce]): Unit = {
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
val tx = unsignedTx.buildUnsignedTx()
- val sharedSig_opt = fundingParams.sharedInput_opt.map(i => i.commitmentFormat match {
- case _: SegwitV0CommitmentFormat => i.sign(channelKeys, tx, unsignedTx.inputDetails).sig
- case _: SimpleTaprootChannelCommitmentFormat => ???
- })
- if (unsignedTx.localInputs.isEmpty) {
- context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt)))
- } else {
- // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will
- // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs.
- val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint))
- val ourWalletOutputs = unsignedTx.localOutputs.flatMap {
- case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript))
- // Non-change outputs may go to an external address (typically during a splice-out).
- // Here we only keep outputs which are ours i.e explicitly go back into our wallet.
- // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call).
- case _: Output.Local.NonChange => None
- }
- // If this is a splice, the PSBT we create must contain the shared input, because if we use taproot wallet inputs
- // we need information about *all* of the transaction's inputs, not just the one we're signing.
- val psbt = unsignedTx.sharedInput_opt.flatMap {
- si => new Psbt(tx).updateWitnessInput(si.outPoint, si.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption
- }.getOrElse(new Psbt(tx))
- context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map {
- response =>
- val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet
- val partiallySignedTx = response.partiallySignedTx
- // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included
- // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid.
- val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum
- val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum
- require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious")
- val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum
- val expectedLocalAmountOut = unsignedTx.localOutputs.map {
- case c: Output.Local.Change => c.amount
- case _: Output.Local.NonChange => 0.sat
- }.sum
- require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious")
- val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness)
- PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt))
- }) {
- case Failure(t) => WalletFailure(t)
- case Success(signedTx) => SignTransactionResult(signedTx)
- }
+ val sharedSig_opt = fundingParams.sharedInput_opt match {
+ case Some(i) => i.sign(fundingParams.channelId, channelKeys, tx, localFundingNonce_opt, remoteFundingNonce_opt, unsignedTx.inputDetails).map(sig => Some(sig))
+ case None => Right(None)
+ }
+ sharedSig_opt match {
+ case Left(f) =>
+ context.self ! WalletFailure(f)
+ case Right(sharedSig_opt) if unsignedTx.localInputs.isEmpty =>
+ context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt)))
+ case Right(sharedSig_opt) =>
+ // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will
+ // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs.
+ val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint))
+ val ourWalletOutputs = unsignedTx.localOutputs.flatMap {
+ case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript))
+ // Non-change outputs may go to an external address (typically during a splice-out).
+ // Here we only keep outputs which are ours i.e explicitly go back into our wallet.
+ // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call).
+ case _: Output.Local.NonChange => None
+ }
+ // If this is a splice, the PSBT we create must contain the shared input, because if we use taproot wallet inputs
+ // we need information about *all* of the transaction's inputs, not just the one we're signing.
+ val psbt = unsignedTx.sharedInput_opt.flatMap {
+ si => new Psbt(tx).updateWitnessInput(si.outPoint, si.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption
+ }.getOrElse(new Psbt(tx))
+ context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map {
+ response =>
+ val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet
+ val partiallySignedTx = response.partiallySignedTx
+ // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included
+ // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid.
+ val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum
+ val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum
+ require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious")
+ val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum
+ val expectedLocalAmountOut = unsignedTx.localOutputs.map {
+ case c: Output.Local.Change => c.amount
+ case _: Output.Local.NonChange => 0.sat
+ }.sum
+ require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious")
+ val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness)
+ PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt))
+ }) {
+ case Failure(t) => WalletFailure(t)
+ case Success(signedTx) => SignTransactionResult(signedTx)
+ }
}
}
@@ -1050,16 +1127,23 @@ object InteractiveTxSigningSession {
return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
}
val sharedSigs_opt = fundingParams.sharedInput_opt.map(sharedInput => {
- sharedInput.commitmentFormat match {
- case _: SegwitV0CommitmentFormat => (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match {
- case (Some(localSig), Some(remoteSig)) =>
- val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey
- Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey)
- case _ =>
- log.info("invalid tx_signatures: missing shared input signatures")
- return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
- }
- case _: SimpleTaprootChannelCommitmentFormat => ???
+ val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey
+ val spliceTx = Transactions.SpliceTx(sharedInput.info, partiallySignedTx.tx.buildUnsignedTx())
+ val signedTx_opt = sharedInput.commitmentFormat match {
+ case _: SegwitV0CommitmentFormat =>
+ (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match {
+ case (Some(localSig), Some(remoteSig)) => Right(spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, IndividualSignature(localSig), IndividualSignature(remoteSig)))
+ case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
+ }
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ (partiallySignedTx.localSigs.previousFundingTxPartialSig_opt, remoteSigs.previousFundingTxPartialSig_opt) match {
+ case (Some(localSig), Some(remoteSig)) => spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, localSig, remoteSig, partiallySignedTx.tx.inputDetails)
+ case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
+ }
+ }
+ signedTx_opt match {
+ case Left(_) => return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
+ case Right(signedTx) => signedTx.txIn(spliceTx.inputIndex).witness
}
})
val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt)
@@ -1101,6 +1185,7 @@ object InteractiveTxSigningSession {
remoteCommitParams: CommitParams,
remoteCommit: RemoteCommit,
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
+ val fundingTxId: TxId = fundingTx.txId
val localCommitIndex: Long = localCommit.fold(_.index, _.index)
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
val nextLocalCommitmentNumber: Long = localCommit match {
@@ -1112,12 +1197,21 @@ object InteractiveTxSigningSession {
def commitInput(fundingKey: PrivateKey): InputInfo = {
val fundingScript = Transactions.makeFundingScript(fundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript
- val fundingOutput = OutPoint(fundingTx.txId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(txOut => txOut.amount == fundingParams.fundingAmount && txOut.publicKeyScript == fundingScript))
+ val fundingOutput = OutPoint(fundingTxId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(txOut => txOut.amount == fundingParams.fundingAmount && txOut.publicKeyScript == fundingScript))
InputInfo(fundingOutput, TxOut(fundingParams.fundingAmount, fundingScript))
}
def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys))
+ /** Nonce for the current commitment, which our peer will need if they must re-send their commit_sig for our current commitment transaction. */
+ def currentCommitNonce_opt(channelKeys: ChannelKeys): Option[LocalNonce] = localCommit match {
+ case Left(_) => Some(NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex))
+ case Right(_) => None
+ }
+
+ /** Nonce for the next commitment, which our peer will need to sign our next commitment transaction. */
+ def nextCommitNonce(channelKeys: ChannelKeys): LocalNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex + 1)
+
def receiveCommitSig(channelParams: ChannelParams, channelKeys: ChannelKeys, remoteCommitSig: CommitSig, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = {
localCommit match {
case Left(unsignedLocalCommit) =>
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala
new file mode 100644
index 0000000..02bc3a1
--- /dev/null
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala
@@ -0,0 +1,33 @@
+package fr.acinq.eclair.crypto
+
+import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
+import fr.acinq.bitcoin.scalacompat.{ByteVector32, Musig2, TxId}
+import fr.acinq.eclair.randomBytes32
+import fr.acinq.eclair.transactions.Transactions.LocalNonce
+import grizzled.slf4j.Logging
+
+object NonceGenerator extends Logging {
+
+ // When using single-funding, we don't have access to the funding tx and remote funding key when creating our first
+ // verification nonce, so we use placeholder values instead. Note that this is fixed with dual-funding.
+ val dummyFundingTxId: TxId = TxId(ByteVector32.Zeroes)
+ val dummyRemoteFundingPubKey: PublicKey = PrivateKey(ByteVector32.One.bytes).publicKey
+
+ /**
+ * @return a deterministic nonce used to sign our local commit tx: its public part is sent to our peer.
+ */
+ def verificationNonce(fundingTxId: TxId, fundingPrivKey: PrivateKey, remoteFundingPubKey: PublicKey, commitIndex: Long): LocalNonce = {
+ val nonces = Musig2.generateNonceWithCounter(commitIndex, fundingPrivKey, Seq(fundingPrivKey.publicKey, remoteFundingPubKey), None, Some(fundingTxId.value))
+ LocalNonce(nonces._1, nonces._2)
+ }
+
+ /**
+ * @return a random nonce used to sign our peer's commit tx.
+ */
+ def signingNonce(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, fundingTxId: TxId): LocalNonce = {
+ val sessionId = randomBytes32()
+ val nonces = Musig2.generateNonce(sessionId, Right(localFundingPubKey), Seq(localFundingPubKey, remoteFundingPubKey), None, Some(fundingTxId.value))
+ LocalNonce(nonces._1, nonces._2)
+ }
+
+}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
index f414d8d..eb51784 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
@@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.scalacompat.{LexicographicalOrdering, SatoshiLong, TxOut}
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
-import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol._
/**
@@ -94,7 +94,8 @@ final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: Feera
def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match {
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat)
- case _ => commitTxFeerate
+ case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => commitTxFeerate
+ case DefaultCommitmentFormat => commitTxFeerate
}
def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc }
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
index 6668aef..f2a9393 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
@@ -17,15 +17,14 @@
package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.Script.LOCKTIME_THRESHOLD
-import fr.acinq.bitcoin.{ScriptTree, SigHash}
+import fr.acinq.bitcoin.ScriptTree
import fr.acinq.bitcoin.SigHash._
import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG}
-import fr.acinq.bitcoin.io.Output
import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey}
import fr.acinq.bitcoin.scalacompat.Script._
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
-import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta}
import scodec.bits.ByteVector
@@ -242,6 +241,7 @@ object Scripts {
/** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script */
def extractPreimageFromHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = {
case ScriptWitness(Seq(ByteVector.empty, _, _, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage)
+ case ScriptWitness(Seq(_, _, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage)
}
/** Extract payment preimages from a (potentially batched) 2nd-stage HTLC transaction's witnesses. */
@@ -257,6 +257,7 @@ object Scripts {
/** Extract the payment preimage from from a fulfilled offered htlc. */
def extractPreimageFromClaimHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = {
case ScriptWitness(Seq(_, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage)
+ case ScriptWitness(Seq(_, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage)
}
/** Extract payment preimages from a (potentially batched) claim HTLC transaction's witnesses. */
@@ -324,7 +325,7 @@ object Scripts {
/**
* Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended.
*/
- def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match {
+ private def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match {
case SIGHASH_DEFAULT | SIGHASH_ALL => sig
case _ => sig :+ sighashType.toByte
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
index e9e7f5b..6c62891 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
@@ -27,6 +27,7 @@ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelSpendSignature
import fr.acinq.eclair.channel.ChannelSpendSignature._
+import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
import fr.acinq.eclair.transactions.CommitmentOutput._
import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT
@@ -191,8 +192,9 @@ object Transactions {
override val claimHtlcPenaltyWeight = 396
}
- case object LegacySimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat {
- override def toString: String = "unsafe_simple_taproot"
+ /** For Phoenix users we sign HTLC transactions with the same feerate as the commit tx to allow broadcasting without wallet inputs. */
+ case object PhoenixSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat {
+ override def toString: String = "simple_taproot_phoenix"
}
case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat {
@@ -362,6 +364,10 @@ object Transactions {
override val desc: String = "commit-tx"
def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
+
+ def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces)
+
+ def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty)
}
/** This transaction collaboratively spends the channel funding output (mutual-close). */
@@ -370,6 +376,10 @@ object Transactions {
val toLocalOutput_opt: Option[TxOut] = toLocalOutputIndex_opt.map(i => tx.txOut(i.toInt))
def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
+
+ def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces)
+
+ def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty)
}
object ClosingTx {
@@ -1537,6 +1547,21 @@ object Transactions {
}
// @formatter:on
+ /**
+ * When sending [[fr.acinq.eclair.wire.protocol.ClosingComplete]], we use a different nonce for each closing transaction we create.
+ * We generate nonces for all variants of the closing transaction for simplicity, even though we never use them all.
+ */
+ case class CloserNonces(localAndRemote: LocalNonce, localOnly: LocalNonce, remoteOnly: LocalNonce)
+
+ object CloserNonces {
+ /** Generate a set of random signing nonces for our closing transactions. */
+ def generate(localFundingKey: PublicKey, remoteFundingKey: PublicKey, fundingTxId: TxId): CloserNonces = CloserNonces(
+ NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId),
+ NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId),
+ NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId),
+ )
+ }
+
/** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */
case class ClosingTxs(localAndRemote_opt: Option[ClosingTx], localOnly_opt: Option[ClosingTx], remoteOnly_opt: Option[ClosingTx]) {
/** Preferred closing transaction for this closing attempt. */
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala
index 1ecc72f..0762700 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala
@@ -233,7 +233,7 @@ private[channel] object ChannelCodecs0 {
val commitSigCodec: Codec[CommitSig] = (
("channelId" | bytes32) ::
- ("signature" | bytes64) ::
+ ("signature" | bytes64.as[ChannelSpendSignature.IndividualSignature]) ::
("htlcSignatures" | listofsignatures) ::
("tlvStream" | provide(TlvStream.empty[CommitSigTlv]))).as[CommitSig]
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
index 6c7f7fa..956db33 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
@@ -48,7 +48,7 @@ private[channel] object ChannelCodecs5 {
private val channelSpendSignatureCodec: Codec[ChannelSpendSignature] = discriminated[ChannelSpendSignature].by(uint8)
.typecase(0x01, bytes64.as[ChannelSpendSignature.IndividualSignature])
- .typecase(0x02, (("partialSig" | bytes32) :: ("nonce" | publicNonce)).as[ChannelSpendSignature.PartialSignatureWithNonce])
+ .typecase(0x02, partialSignatureWithNonce)
private def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList)
@@ -81,7 +81,7 @@ private[channel] object ChannelCodecs5 {
.typecase(0x00, provide(Transactions.DefaultCommitmentFormat))
.typecase(0x01, provide(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat))
.typecase(0x02, provide(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
- .typecase(0x03, provide(Transactions.LegacySimpleTaprootChannelCommitmentFormat))
+ .typecase(0x03, provide(Transactions.PhoenixSimpleTaprootChannelCommitmentFormat))
.typecase(0x04, provide(Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat))
private val localChannelParamsCodec: Codec[LocalChannelParams] = (
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
index 7b561d3..13e0b76 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
@@ -16,7 +16,9 @@
package fr.acinq.eclair.wire.protocol
-import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi, TxId}
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
+import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId}
+import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tmillisatoshi}
@@ -53,7 +55,7 @@ object ChannelTlv {
val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = tlvField(bytes)
/** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */
- case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv
+ case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv
val channelTypeCodec: Codec[ChannelTypeTlv] = tlvField(bytes.xmap[ChannelTypeTlv](
b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())),
@@ -89,6 +91,16 @@ object ChannelTlv {
*/
case class UseFeeCredit(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with SpliceInitTlv
+ /** Verification nonce used for the next commitment transaction that will be signed (when using taproot channels). */
+ case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with ChannelReadyTlv with ClosingTlv
+
+ val nextLocalNonceCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce)
+
+ /** Partial signature along with the signer's nonce, which is usually randomly created at signing time (when using taproot channels). */
+ case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends FundingCreatedTlv with FundingSignedTlv with ClosingTlv
+
+ val partialSignatureWithNonceCodec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce)
+
}
object OpenChannelTlv {
@@ -98,6 +110,7 @@ object OpenChannelTlv {
val openTlvCodec: Codec[TlvStream[OpenChannelTlv]] = tlvStream(discriminated[OpenChannelTlv].by(varint)
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
+ .typecase(UInt64(4), nextLocalNonceCodec)
)
}
@@ -109,6 +122,7 @@ object AcceptChannelTlv {
val acceptTlvCodec: Codec[TlvStream[AcceptChannelTlv]] = tlvStream(discriminated[AcceptChannelTlv].by(varint)
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
+ .typecase(UInt64(4), nextLocalNonceCodec)
)
}
@@ -169,6 +183,7 @@ object SpliceInitTlv {
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), requestFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
+ .typecase(UInt64(0x47000011), tlvField(channelTypeCodec.as[ChannelTypeTlv]))
)
}
@@ -182,6 +197,7 @@ object SpliceAckTlv {
.typecase(UInt64(1339), provideFundingCodec)
.typecase(UInt64(41042), feeCreditUsedCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
+ .typecase(UInt64(0x47000011), tlvField(channelTypeCodec.as[ChannelTypeTlv]))
)
}
@@ -208,13 +224,17 @@ object AcceptDualFundedChannelTlv {
sealed trait FundingCreatedTlv extends Tlv
object FundingCreatedTlv {
- val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint))
+ val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint)
+ .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec)
+ )
}
sealed trait FundingSignedTlv extends Tlv
object FundingSignedTlv {
- val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint))
+ val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)
+ .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec)
+ )
}
sealed trait ChannelReadyTlv extends Tlv
@@ -227,6 +247,7 @@ object ChannelReadyTlv {
val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint)
.typecase(UInt64(1), channelAliasTlvCodec)
+ .typecase(UInt64(4), ChannelTlv.nextLocalNonceCodec)
)
}
@@ -238,6 +259,19 @@ object ChannelReestablishTlv {
case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
+ /**
+ * When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment
+ * which our peer will need to re-send a commit sig for our current commitment transaction spending the interactive tx.
+ */
+ case class CurrentCommitNonceTlv(nonce: IndividualNonce) extends ChannelReestablishTlv
+
+ /**
+ * Verification nonces used for the next commitment transaction, when using taproot channels.
+ * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the
+ * corresponding fundingTxId.
+ */
+ case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv
+
object NextFundingTlv {
val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
}
@@ -245,14 +279,25 @@ object ChannelReestablishTlv {
object YourLastFundingLockedTlv {
val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
}
+
object MyCurrentFundingLockedTlv {
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
}
+ object CurrentCommitNonceTlv {
+ val codec: Codec[CurrentCommitNonceTlv] = tlvField("current_commit_nonce" | publicNonce)
+ }
+
+ object NextLocalNoncesTlv {
+ val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList))
+ }
+
val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
.typecase(UInt64(0), NextFundingTlv.codec)
.typecase(UInt64(1), YourLastFundingLockedTlv.codec)
.typecase(UInt64(3), MyCurrentFundingLockedTlv.codec)
+ .typecase(UInt64(22), NextLocalNoncesTlv.codec)
+ .typecase(UInt64(24), CurrentCommitNonceTlv.codec)
)
}
@@ -265,7 +310,14 @@ object UpdateFeeTlv {
sealed trait ShutdownTlv extends Tlv
object ShutdownTlv {
- val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint))
+ /** When closing taproot channels, local nonce that will be used to sign the remote closing transaction. */
+ case class ShutdownNonce(nonce: IndividualNonce) extends ShutdownTlv
+
+ private val shutdownNonceCodec: Codec[ShutdownNonce] = tlvField(publicNonce)
+
+ val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint)
+ .typecase(UInt64(8), shutdownNonceCodec)
+ )
}
sealed trait ClosingSignedTlv extends Tlv
@@ -286,18 +338,60 @@ sealed trait ClosingTlv extends Tlv
object ClosingTlv {
/** Signature for a closing transaction containing only the closer's output. */
- case class CloserOutputOnly(sig: ByteVector64) extends ClosingTlv
+ case class CloserOutputOnly(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv
/** Signature for a closing transaction containing only the closee's output. */
- case class CloseeOutputOnly(sig: ByteVector64) extends ClosingTlv
+ case class CloseeOutputOnly(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv
/** Signature for a closing transaction containing the closer and closee's outputs. */
- case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv
+ case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv
+}
+
+sealed trait ClosingCompleteTlv extends ClosingTlv
+
+object ClosingCompleteTlv {
+ /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */
+ case class CloserOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv
+
+ /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */
+ case class CloseeOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv
+
+ /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */
+ case class CloserAndCloseeOutputsPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv
- val closingTlvCodec: Codec[TlvStream[ClosingTlv]] = tlvStream(discriminated[ClosingTlv].by(varint)
- .typecase(UInt64(1), tlvField(bytes64.as[CloserOutputOnly]))
- .typecase(UInt64(2), tlvField(bytes64.as[CloseeOutputOnly]))
- .typecase(UInt64(3), tlvField(bytes64.as[CloserAndCloseeOutputs]))
+ val closingCompleteTlvCodec: Codec[TlvStream[ClosingCompleteTlv]] = tlvStream(discriminated[ClosingCompleteTlv].by(varint)
+ .typecase(UInt64(1), tlvField(bytes64.as[ClosingTlv.CloserOutputOnly]))
+ .typecase(UInt64(2), tlvField(bytes64.as[ClosingTlv.CloseeOutputOnly]))
+ .typecase(UInt64(3), tlvField(bytes64.as[ClosingTlv.CloserAndCloseeOutputs]))
+ .typecase(UInt64(5), tlvField(partialSignatureWithNonce.as[CloserOutputOnlyPartialSignature]))
+ .typecase(UInt64(6), tlvField(partialSignatureWithNonce.as[CloseeOutputOnlyPartialSignature]))
+ .typecase(UInt64(7), tlvField(partialSignatureWithNonce.as[CloserAndCloseeOutputsPartialSignature]))
)
+}
+
+sealed trait ClosingSigTlv extends ClosingTlv
+
+object ClosingSigTlv {
+ /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */
+ case class CloserOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv
+
+ /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */
+ case class CloseeOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv
+ /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */
+ case class CloserAndCloseeOutputsPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv
+
+ /** When closing taproot channels, local nonce that will be used to sign the next remote closing transaction. */
+ case class NextCloseeNonce(nonce: IndividualNonce) extends ClosingSigTlv
+
+ val closingSigTlvCodec: Codec[TlvStream[ClosingSigTlv]] = tlvStream(discriminated[ClosingSigTlv].by(varint)
+ .typecase(UInt64(1), tlvField(bytes64.as[ClosingTlv.CloserOutputOnly]))
+ .typecase(UInt64(2), tlvField(bytes64.as[ClosingTlv.CloseeOutputOnly]))
+ .typecase(UInt64(3), tlvField(bytes64.as[ClosingTlv.CloserAndCloseeOutputs]))
+ .typecase(UInt64(5), tlvField(bytes32.as[CloserOutputOnlyPartialSignature]))
+ .typecase(UInt64(6), tlvField(bytes32.as[CloseeOutputOnlyPartialSignature]))
+ .typecase(UInt64(7), tlvField(bytes32.as[CloserAndCloseeOutputsPartialSignature]))
+ .typecase(UInt64(22), tlvField(publicNonce.as[NextCloseeNonce]))
+ )
}
+
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala
index ea9d823..29894f8 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala
@@ -20,6 +20,7 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
+import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases}
import fr.acinq.eclair.crypto.Mac32
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId}
@@ -163,6 +164,8 @@ object CommonCodecs {
(wire: BitVector) => bytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE).decode(wire).map(_.map(b => new IndividualNonce(b.toArray)))
)
+ val partialSignatureWithNonce: Codec[PartialSignatureWithNonce] = (bytes32 :: publicNonce).as[PartialSignatureWithNonce]
+
val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b))
val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala
index ee93246..2471bd3 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala
@@ -16,8 +16,11 @@
package fr.acinq.eclair.wire.protocol
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
+import fr.acinq.bitcoin.scalacompat.TxId
import fr.acinq.eclair.UInt64
+import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16}
@@ -94,7 +97,15 @@ object CommitSigTlv {
val codec: Codec[BatchTlv] = tlvField(tu16)
}
+ /** Partial signature signature for the current commitment transaction, along with the signing nonce used (when using taproot channels). */
+ case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends CommitSigTlv
+
+ object PartialSignatureWithNonceTlv {
+ val codec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce)
+ }
+
val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint)
+ .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec)
.typecase(UInt64(0x47010005), BatchTlv.codec)
)
@@ -103,5 +114,19 @@ object CommitSigTlv {
sealed trait RevokeAndAckTlv extends Tlv
object RevokeAndAckTlv {
- val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint))
+
+ /**
+ * Verification nonces used for the next commitment transaction, when using taproot channels.
+ * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the
+ * corresponding fundingTxId.
+ */
+ case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends RevokeAndAckTlv
+
+ object NextLocalNoncesTlv {
+ val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList))
+ }
+
+ val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint)
+ .typecase(UInt64(22), NextLocalNoncesTlv.codec)
+ )
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
index 96696d8..d13c38b 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
@@ -16,12 +16,14 @@
package fr.acinq.eclair.wire.protocol
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.{ByteVector64, TxId}
import fr.acinq.eclair.UInt64
-import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, txIdAsHash, varint}
+import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
+import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream}
import scodec.Codec
-import scodec.codecs.discriminated
+import scodec.codecs.{bitsRemaining, discriminated, optional}
/**
* Created by t-bast on 08/04/2022.
@@ -60,7 +62,23 @@ object TxRemoveOutputTlv {
sealed trait TxCompleteTlv extends Tlv
object TxCompleteTlv {
- val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint))
+ /**
+ * Musig2 nonces exchanged during an interactive tx session, when using a taproot channel or upgrading a channel to
+ * use taproot.
+ *
+ * @param commitNonce the sender's verification nonce for the current commit tx spending the interactive tx.
+ * @param nextCommitNonce the sender's verification nonce for the next commit tx spending the interactive tx.
+ * @param fundingNonce_opt when splicing a taproot channel, the sender's random signing nonce for the previous funding output.
+ */
+ case class Nonces(commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]) extends TxCompleteTlv
+
+ object Nonces {
+ val codec: Codec[Nonces] = tlvField((publicNonce :: publicNonce :: optional(bitsRemaining, publicNonce)).as[Nonces])
+ }
+
+ val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)
+ .typecase(UInt64(4), Nonces.codec)
+ )
}
sealed trait TxSignaturesTlv extends Tlv
@@ -69,7 +87,11 @@ object TxSignaturesTlv {
/** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */
case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv
+ /** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */
+ case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv
+
val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)
+ .typecase(UInt64(2), tlvField(partialSignatureWithNonce.as[PreviousFundingTxPartialSig]))
.typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig]))
)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
index cd9f4da..f506a3c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
@@ -17,6 +17,7 @@
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.ScriptWitness
+import fr.acinq.eclair.channel.ChannelSpendSignature
import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.{Features, InitFeature, KamonExt}
@@ -233,7 +234,7 @@ object LightningMessageCodecs {
("closeeScriptPubKey" | varsizebinarydata) ::
("fees" | satoshi) ::
("lockTime" | uint32) ::
- ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingComplete]
+ ("tlvStream" | ClosingCompleteTlv.closingCompleteTlvCodec)).as[ClosingComplete]
val closingSigCodec: Codec[ClosingSig] = (
("channelId" | bytes32) ::
@@ -241,7 +242,7 @@ object LightningMessageCodecs {
("closeeScriptPubKey" | varsizebinarydata) ::
("fees" | satoshi) ::
("lockTime" | uint32) ::
- ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingSig]
+ ("tlvStream" | ClosingSigTlv.closingSigTlvCodec)).as[ClosingSig]
val updateAddHtlcCodec: Codec[UpdateAddHtlc] = (
("channelId" | bytes32) ::
@@ -273,7 +274,7 @@ object LightningMessageCodecs {
val commitSigCodec: Codec[CommitSig] = (
("channelId" | bytes32) ::
- ("signature" | bytes64) ::
+ ("signature" | bytes64.as[ChannelSpendSignature.IndividualSignature]) ::
("htlcSignatures" | listofsignatures) ::
("tlvStream" | CommitSigTlv.commitSigTlvCodec)).as[CommitSig]
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
index 0c04d26..74129f5 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
@@ -18,10 +18,12 @@ package fr.acinq.eclair.wire.protocol
import com.google.common.base.Charsets
import com.google.common.net.InetAddresses
+import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, OutPoint, Satoshi, SatoshiLong, ScriptWitness, Transaction, TxId}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
-import fr.acinq.eclair.channel.{ChannelFlags, ChannelType}
+import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce}
+import fr.acinq.eclair.channel.{ChannelFlags, ChannelSpendSignature, ChannelType}
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, isAsciiPrintable}
@@ -116,18 +118,32 @@ case class TxRemoveOutput(channelId: ByteVector32,
tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId
case class TxComplete(channelId: ByteVector32,
- tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId
+ tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId {
+ val nonces_opt: Option[TxCompleteTlv.Nonces] = tlvStream.get[TxCompleteTlv.Nonces]
+}
+
+object TxComplete {
+ def apply(channelId: ByteVector32, commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]): TxComplete =
+ TxComplete(channelId, TlvStream(TxCompleteTlv.Nonces(commitNonce, nextCommitNonce, fundingNonce_opt)))
+}
case class TxSignatures(channelId: ByteVector32,
txId: TxId,
witnesses: Seq[ScriptWitness],
tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId {
val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig)
+ val previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxPartialSig].map(_.partialSigWithNonce)
}
object TxSignatures {
- def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = {
- TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv]))
+ def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = {
+ val tlvs: Set[TxSignaturesTlv] = Set(
+ previousFundingSig_opt.map {
+ case IndividualSignature(sig) => TxSignaturesTlv.PreviousFundingTxSig(sig)
+ case partialSig: PartialSignatureWithNonce => TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig)
+ }
+ ).flatten
+ TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs))
}
}
@@ -187,6 +203,8 @@ case class ChannelReestablish(channelId: ByteVector32,
val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId)
val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId)
+ val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelReestablishTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty)
+ val currentCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelReestablishTlv.CurrentCommitNonceTlv].map(_.nonce)
}
case class OpenChannel(chainHash: BlockHash,
@@ -210,6 +228,7 @@ case class OpenChannel(chainHash: BlockHash,
tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash {
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script)
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
+ val commitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce)
}
case class AcceptChannel(temporaryChannelId: ByteVector32,
@@ -229,6 +248,7 @@ case class AcceptChannel(temporaryChannelId: ByteWhy this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.