Extract `CommitParams` to individual commitments (#3118)
What changed, and why it matters
This is a large internal refactoring of Eclair's Lightning channel data structures. It moves commitment-specific settings (such as dust limits and HTLC limits) from the overall channel parameters into each individual commitment, so future splices can change them. It also removes the full confirmed funding transaction from the local database record, adds a cap on closing fee rates for RBF attempts, and introduces a startup-blocking guard when the database serialization format changes. The commit is not described by the vendor as a security fix; it is a forward-looking infrastructure change with some safety mechanisms.
Treat this as a high-risk infrastructure change rather than an active vulnerability. Operators should not downgrade across this commit because channel database serialization is changed. Reviewers should verify that the new startup guard correctly prevents accidental startup on incompatible DB states, that `ConfirmedFundingTx` removal does not break on-chain transaction reconstruction or fee bumping, and that the TODOs around revoked-commit penalty parameters are addressed before dynamic commitment updates are enabled.
Security signals we found
Large data-model refactor of channel state serialization
New channel codec version (version5) and startup guard against unsupported codec versions
Removal of full confirmed funding transaction from local DB record
Addition of maxClosingFeerate_opt to limit RBF closing feerate
Per-commitment parameters now stored per commitment, enabling future dynamic updates
Multiple TODO comments noting that revoked-commit penalty paths may use wrong toSelfDelay/commitmentFormat after future splices
Evidence from the diff
The commit refactors Commitments and ChannelParams: per-commitment fields (dustLimit, maxHtlcValueInFlightMsat, htlcMinimum, toRemoteDelay, maxAcceptedHtlcs) are extracted into a new CommitParams stored inside each Commitment. LocalChannelParams/RemoteChannelParams now only hold lifetime parameters. ConfirmedFundingTx no longer stores the full Transaction, only the TxOut, so the node must fetch the funding tx from the blockchain when needed. A maxClosingFeerate_opt is added to DATA_CLOSING to bound RBF fee escalation. A startup guard is added in Boot.scala to refuse to start if the channel codec version is unsupported unless an override is set. Channel type features are made non-permanent, and ChannelFeatures construction is adjusted accordingly. The change touches codecs, state machines, transaction building, and tests.
Changed components
eclair-core channel state and commitmentschannel codecs (version0-version5)interactive transaction / splice builderclosing transaction and RBF handlingnode startup (Boot.scala)channel feature negotiationInspect captured patch +2334 / −1373
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 080a2e4..f594e78 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
@@ -36,17 +36,15 @@ object FeatureSupport {
/** Not a sealed trait, so it can be extended by plugins. */
trait Feature {
-
def rfcName: String
def mandatory: Int
def optional: Int = mandatory + 1
-
def supportBit(support: FeatureSupport): Int = support match {
case Mandatory => mandatory
case Optional => optional
}
- override def toString = rfcName
+ override def toString: String = rfcName
}
/** Feature scope as defined in Bolt 9. */
@@ -68,11 +66,9 @@ trait Bolt12Feature extends InvoiceFeature
*/
trait PermanentChannelFeature extends InitFeature // <- not in the spec
/**
- * Permanent channel feature negotiated in the channel type. Those features take precedence over permanent channel
- * features negotiated in init messages. For example, if the channel type is option_static_remotekey, then even if
- * the option_anchor_outputs feature is supported by both peers, it won't apply to the channel.
+ * Features that can be included in the [[fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv]].
*/
-trait ChannelTypeFeature extends PermanentChannelFeature
+trait ChannelTypeFeature extends InitFeature
// @formatter:on
case class UnknownFeature(bitIndex: Int)
@@ -275,6 +271,7 @@ object Features {
val rfcName = "option_attribution_data"
val mandatory = 36
}
+
case object OnionMessages extends Feature with InitFeature with NodeFeature {
val rfcName = "option_onion_messages"
val mandatory = 38
@@ -290,7 +287,7 @@ object Features {
val mandatory = 44
}
- case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
+ case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature {
val rfcName = "option_scid_alias"
val mandatory = 46
}
@@ -300,7 +297,7 @@ object Features {
val mandatory = 48
}
- case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
+ case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature {
val rfcName = "option_zeroconf"
val mandatory = 50
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala
index dbd6120..5196498 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala
@@ -41,7 +41,7 @@ trait SpendFromChannelAddress {
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
- inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt), ByteVector.empty)
+ inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
// classify as splice, doesn't really matter
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala
index dc62645..24ea89d 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala
@@ -176,8 +176,8 @@ object CheckBalance {
case d: DATA_SHUTDOWN => this.copy(shutdown = this.shutdown.addChannelBalance(d.commitments))
// If one of our closing transactions is in the mempool or recently confirmed, and thus included in our on-chain
// balance, we ignore this channel in our off-chain balance to avoid counting it twice.
- case d: DATA_NEGOTIATING if recentlySpentInputs.contains(d.commitments.latest.commitInput.outPoint) => this
- case d: DATA_NEGOTIATING_SIMPLE if recentlySpentInputs.contains(d.commitments.latest.commitInput.outPoint) => this
+ case d: DATA_NEGOTIATING if recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this
+ case d: DATA_NEGOTIATING_SIMPLE if recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this
// Otherwise, that means the closing transactions aren't in the mempool yet, so we include our off-chain balance.
case d: DATA_NEGOTIATING => this.copy(negotiating = this.negotiating.addChannelBalance(d.commitments))
case d: DATA_NEGOTIATING_SIMPLE => this.copy(negotiating = this.negotiating.addChannelBalance(d.commitments))
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 93c9c76..ccead6c 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
@@ -20,7 +20,6 @@ import akka.actor.{ActorRef, PossiblyHarmful, typed}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut}
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
-import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
import fr.acinq.eclair.io.Peer
@@ -430,6 +429,7 @@ case class RevokedCommitPublished(commitTx: Transaction, localOutput_opt: Option
case class ShortIdAliases(localAlias: Alias, remoteAlias_opt: Option[Alias])
sealed trait LocalFundingStatus {
+ /** While the transaction is unconfirmed, we keep the funding transaction (if available) to allow rebroadcasting. */
def signedTx_opt: Option[Transaction]
/** We store local signatures for the purpose of retransmitting if the funding/splicing flow is interrupted. */
def localSigs_opt: Option[TxSignatures]
@@ -458,8 +458,8 @@ object LocalFundingStatus {
case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with Locked {
override val signedTx_opt: Option[Transaction] = Some(tx)
}
- case class ConfirmedFundingTx(tx: Transaction, shortChannelId: RealShortChannelId, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked {
- override val signedTx_opt: Option[Transaction] = Some(tx)
+ case class ConfirmedFundingTx(txOut: TxOut, shortChannelId: RealShortChannelId, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked {
+ override val signedTx_opt: Option[Transaction] = None
}
}
@@ -567,6 +567,9 @@ final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INI
val channelId: ByteVector32 = initFunder.temporaryChannelId
}
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams: ChannelParams,
+ channelType: SupportedChannelType,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
fundingAmount: Satoshi,
pushAmount: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
@@ -574,16 +577,24 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams: ChannelParams,
remoteFirstPerCommitmentPoint: PublicKey,
replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
val channelId: ByteVector32 = channelParams.channelId
+ val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
}
final case class DATA_WAIT_FOR_FUNDING_CREATED(channelParams: ChannelParams,
+ channelType: SupportedChannelType,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
fundingAmount: Satoshi,
pushAmount: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey) extends TransientChannelData {
val channelId: ByteVector32 = channelParams.channelId
+ val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
}
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelParams: ChannelParams,
+ channelType: SupportedChannelType,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
remoteFundingPubKey: PublicKey,
fundingTx: Transaction,
fundingTxFee: Satoshi,
@@ -593,6 +604,7 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelParams: ChannelParams,
lastSent: FundingCreated,
replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
val channelId: ByteVector32 = channelParams.channelId
+ val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
}
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm
@@ -610,6 +622,8 @@ final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANN
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
channelParams: ChannelParams,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
secondRemotePerCommitmentPoint: PublicKey,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
@@ -620,8 +634,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams,
secondRemotePerCommitmentPoint: PublicKey,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
- signingSession: InteractiveTxSigningSession.WaitingForSigs,
- remoteChannelData_opt: Option[ByteVector]) extends ChannelDataWithoutCommitments
+ signingSession: InteractiveTxSigningSession.WaitingForSigs) extends ChannelDataWithoutCommitments
final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
@@ -629,9 +642,9 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
lastChecked: BlockHeight, // last time we checked if the channel was double-spent
status: DualFundingStatus,
deferred: Option[ChannelReady]) extends ChannelDataWithCommitments {
- def allFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = commitments.active.map(_.localFundingStatus).collect { case fundingTx: DualFundedUnconfirmedFundingTx => fundingTx }
- def latestFundingTx: DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx]
- def previousFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = allFundingTxs diff Seq(latestFundingTx)
+ def allFundingTxs: Seq[LocalFundingStatus.DualFundedUnconfirmedFundingTx] = commitments.active.map(_.localFundingStatus).collect { case fundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx => fundingTx }
+ def latestFundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx]
+ def previousFundingTxs: Seq[LocalFundingStatus.DualFundedUnconfirmedFundingTx] = allFundingTxs diff Seq(latestFundingTx)
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, aliases: ShortIdAliases) extends ChannelDataWithCommitments
@@ -639,10 +652,10 @@ final case class DATA_NORMAL(commitments: Commitments,
aliases: ShortIdAliases,
lastAnnouncement_opt: Option[ChannelAnnouncement],
channelUpdate: ChannelUpdate,
+ spliceStatus: SpliceStatus,
localShutdown: Option[Shutdown],
remoteShutdown: Option[Shutdown],
- closeStatus_opt: Option[CloseStatus],
- spliceStatus: SpliceStatus) extends ChannelDataWithCommitments {
+ closeStatus_opt: Option[CloseStatus]) extends ChannelDataWithCommitments {
val lastAnnouncedCommitment_opt: Option[AnnouncedCommitment] = lastAnnouncement_opt.flatMap(ann => commitments.resolveCommitment(ann.shortChannelId).map(c => AnnouncedCommitment(c, ann)))
val lastAnnouncedFundingTxId_opt: Option[TxId] = lastAnnouncedCommitment_opt.map(_.fundingTxId)
val isNegotiatingQuiescence: Boolean = spliceStatus.isNegotiatingQuiescence
@@ -675,24 +688,21 @@ final case class DATA_CLOSING(commitments: Commitments,
remoteCommitPublished: Option[RemoteCommitPublished] = None,
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
futureRemoteCommitPublished: Option[RemoteCommitPublished] = None,
- revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends ChannelDataWithCommitments {
+ revokedCommitPublished: List[RevokedCommitPublished] = Nil,
+ maxClosingFeerate_opt: Option[FeeratePerKw] = None) extends ChannelDataWithCommitments {
val spendingTxs: List[Transaction] = mutualClosePublished.map(_.tx) ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx)
require(spendingTxs.nonEmpty, "there must be at least one tx published in this state")
}
final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends ChannelDataWithCommitments
+/** Local params that apply for the channel's lifetime. */
case class LocalChannelParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
- dustLimit: Satoshi,
- maxHtlcValueInFlightMsat: UInt64,
// Channel reserve applied to the remote peer, if we're not using [[Features.DualFunding]] (in
// which case the reserve is set to 1%). If the channel is spliced, this initial value will be
// ignored in favor of a 1% reserve of the resulting capacity.
initialRequestedChannelReserve_opt: Option[Satoshi],
- htlcMinimum: MilliSatoshi,
- toRemoteDelay: CltvExpiryDelta,
- maxAcceptedHtlcs: Int,
isChannelOpener: Boolean,
paysCommitTxFees: Boolean,
upfrontShutdownScript_opt: Option[ByteVector],
@@ -704,18 +714,12 @@ case class LocalChannelParams(nodeId: PublicKey,
// The node responsible for the commit tx fees is also the node paying the mutual close fees.
// The other node's balance may be empty, which wouldn't allow them to pay the closing fees.
val paysClosingFees: Boolean = paysCommitTxFees
-
- val proposedCommitParams: ProposedCommitParams = ProposedCommitParams(dustLimit, htlcMinimum, maxHtlcValueInFlightMsat, maxAcceptedHtlcs, toRemoteDelay)
}
+/** Remote params that apply for the channel's lifetime. */
case class RemoteChannelParams(nodeId: PublicKey,
- dustLimit: Satoshi,
- maxHtlcValueInFlightMsat: UInt64,
// See comment in LocalChannelParams for details.
initialRequestedChannelReserve_opt: Option[Satoshi],
- htlcMinimum: MilliSatoshi,
- toRemoteDelay: CltvExpiryDelta,
- maxAcceptedHtlcs: Int,
revocationBasepoint: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
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 3ab4c20..dc38e86 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
@@ -25,22 +25,10 @@ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeatur
/**
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
- * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
- * channel is upgraded or closed.
+ * Even if one of these features is later disabled at the connection level, it will still apply to the channel.
*/
case class ChannelFeatures(features: Set[PermanentChannelFeature]) {
- /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
- val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)
- /** Legacy option_anchor_outputs is used for Phoenix, because Phoenix doesn't have an on-chain wallet to pay for fees. */
- val commitmentFormat: CommitmentFormat = if (hasFeature(Features.AnchorOutputs)) {
- UnsafeLegacyAnchorOutputsCommitmentFormat
- } else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
- ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
- } else {
- DefaultCommitmentFormat
- }
-
def hasFeature(feature: PermanentChannelFeature): Boolean = features.contains(feature)
override def toString: String = features.mkString(",")
@@ -51,19 +39,20 @@ object ChannelFeatures {
def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features))
- /** Enrich the channel type with other permanent features that will be applied to the channel. */
+ /** Configure permanent channel features based on local and remote feature. */
def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): ChannelFeatures = {
- val additionalPermanentFeatures = Features.knownFeatures.collect {
+ val permanentFeatures = Features.knownFeatures.collect {
// If we both support 0-conf or scid_alias, we use it even if it wasn't in the channel-type.
+ // Note that we cannot use scid_alias if the channel is announced.
case Features.ScidAlias if Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel => Some(Features.ScidAlias)
+ case Features.ScidAlias => None
case Features.ZeroConf if Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroConf) => Some(Features.ZeroConf)
- // Other channel-type features are negotiated in the channel-type, we ignore their value from the init message.
- case _: ChannelTypeFeature => None
- // We add all other permanent channel features that aren't negotiated as part of the channel-type.
+ // We add all other permanent channel features that we both support.
case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f)
}.flatten
- val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures
- ChannelFeatures(allPermanentFeatures: _*)
+ // Some permanent features can be negotiated as part of the channel-type.
+ val channelTypeFeatures = channelType.features.collect { case f: PermanentChannelFeature => f }
+ ChannelFeatures(permanentFeatures ++ channelTypeFeatures)
}
}
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 4d1a2e5..729f989 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
@@ -3,7 +3,7 @@ package fr.acinq.eclair.channel
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
-import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Transaction, TxId}
+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.Helpers.Closing
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
@@ -25,28 +25,16 @@ case class ChannelParams(channelId: ByteVector32,
channelFeatures: ChannelFeatures,
localParams: LocalChannelParams, remoteParams: RemoteChannelParams,
channelFlags: ChannelFlags) {
- require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures")
require(channelFeatures.hasFeature(Features.DualFunding) == localParams.initialRequestedChannelReserve_opt.isEmpty, "custom local channel reserve is incompatible with dual-funded channels")
require(channelFeatures.hasFeature(Features.DualFunding) == remoteParams.initialRequestedChannelReserve_opt.isEmpty, "custom remote channel reserve is incompatible with dual-funded channels")
- val commitmentFormat: CommitmentFormat = channelFeatures.commitmentFormat
val announceChannel: Boolean = channelFlags.announceChannel
-
val localNodeId: PublicKey = localParams.nodeId
val remoteNodeId: PublicKey = remoteParams.nodeId
-
- val localCommitParams: CommitParams = CommitParams(localParams.dustLimit, localParams.htlcMinimum, localParams.maxHtlcValueInFlightMsat, localParams.maxAcceptedHtlcs, remoteParams.toRemoteDelay)
- val remoteCommitParams: CommitParams = CommitParams(remoteParams.dustLimit, remoteParams.htlcMinimum, remoteParams.maxHtlcValueInFlightMsat, remoteParams.maxAcceptedHtlcs, localParams.toRemoteDelay)
-
- // We can safely cast to millisatoshis since we verify that it's less than a valid millisatoshi amount.
- val maxHtlcValueInFlight: MilliSatoshi = Seq(localParams.maxHtlcValueInFlightMsat, remoteParams.maxHtlcValueInFlightMsat, UInt64(MilliSatoshi.MaxMoney.toLong)).min.toBigInt.toLong.msat
-
// If we've set the 0-conf feature bit for this peer, we will always use 0-conf with them.
val zeroConf: Boolean = localParams.initFeatures.hasFeature(Features.ZeroConf)
- /**
- * We update local/global features at reconnection
- */
+ /** We update local/global features at reconnection. */
def updateFeatures(localInit: Init, remoteInit: Init): ChannelParams = copy(
localParams = localParams.copy(initFeatures = localInit.features),
remoteParams = remoteParams.copy(initFeatures = remoteInit.features),
@@ -58,20 +46,6 @@ case class ChannelParams(channelId: ByteVector32,
*/
def minDepth(defaultMinDepth: Int): Option[Int] = if (zeroConf) None else Some(defaultMinDepth)
- /** Channel reserve that applies to our funds. */
- def localChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) {
- (capacity / 100).max(remoteCommitParams.dustLimit)
- } else {
- remoteParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams
- }
-
- /** Channel reserve that applies to our peer's funds. */
- def remoteChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) {
- (capacity / 100).max(localCommitParams.dustLimit)
- } else {
- localParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams
- }
-
/**
* @param localScriptPubKey local script pubkey (provided in CMD_CLOSE, as an upfront shutdown script, or set to the current final onchain script)
* @return an exception if the provided script is not valid
@@ -189,51 +163,51 @@ object ChannelSpendSignature {
* The local commitment maps to a commitment transaction that we can sign and broadcast if necessary.
* The [[htlcRemoteSigs]] are stored in the order in which HTLC outputs appear in the commitment transaction.
*/
-case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64])
+case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64])
object LocalCommit {
- def fromCommitSig(params: ChannelParams, commitKeys: LocalCommitmentKeys, fundingTxId: TxId,
+ def fromCommitSig(channelParams: ChannelParams, commitParams: CommitParams, commitKeys: LocalCommitmentKeys, fundingTxId: TxId,
fundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
- commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec): Either[ChannelException, LocalCommit] = {
- val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(params, params.localCommitParams, commitKeys, localCommitIndex, fundingKey, remoteFundingPubKey, commitInput, spec)
- val remoteCommitSigOk = params.commitmentFormat match {
+ 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 => ???
}
if (!remoteCommitSigOk) {
- return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, localCommitIndex, localCommitTx.tx))
+ return Left(InvalidCommitmentSignature(channelParams.channelId, fundingTxId, localCommitIndex, localCommitTx.tx))
}
- val commitTxRemoteSig = params.commitmentFormat match {
+ 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(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
+ return Left(HtlcSigCountMismatch(channelParams.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
}
val htlcRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map {
case (htlcTx: HtlcTx, remoteSig) =>
if (!htlcTx.checkRemoteSig(commitKeys, remoteSig)) {
- return Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid))
+ return Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid))
}
remoteSig
}
- Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, localCommitTx.input, commitTxRemoteSig, htlcRemoteSigs))
+ Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commitTxRemoteSig, 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(params: ChannelParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = {
+ def sign(channelParams: ChannelParams, commitParams: CommitParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, commitmentFormat: CommitmentFormat): CommitSig = {
val fundingKey = channelKeys.fundingKey(fundingTxIndex)
- val commitKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint)
- val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, params.remoteCommitParams, commitKeys, index, fundingKey, remoteFundingPubKey, commitInput, spec)
+ val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentPoint, commitmentFormat)
+ val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(channelParams, commitParams, commitKeys, index, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec)
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
val htlcSigs = sortedHtlcTxs.map(_.localSig(commitKeys))
- params.commitmentFormat match {
+ commitmentFormat match {
case _: SegwitV0CommitmentFormat =>
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig
- CommitSig(params.channelId, sig, htlcSigs.toList)
+ CommitSig(channelParams.channelId, sig, htlcSigs.toList)
case _: SimpleTaprootChannelCommitmentFormat => ???
}
}
@@ -268,28 +242,51 @@ case class CommitTxIds(localCommitTxId: TxId, remoteCommitTxId: TxId, nextRemote
*/
case class Commitment(fundingTxIndex: Long,
firstRemoteCommitIndex: Long,
+ fundingInput: OutPoint,
+ fundingAmount: Satoshi,
remoteFundingPubKey: PublicKey,
- localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus,
- localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) {
- val commitInput: InputInfo = localCommit.input
- val fundingTxId: TxId = commitInput.outPoint.txid
+ localFundingStatus: LocalFundingStatus,
+ remoteFundingStatus: RemoteFundingStatus,
+ commitmentFormat: CommitmentFormat,
+ localCommitParams: CommitParams,
+ localCommit: LocalCommit,
+ remoteCommitParams: CommitParams,
+ remoteCommit: RemoteCommit,
+ nextRemoteCommit_opt: Option[NextRemoteCommit]) {
+ val fundingTxId: TxId = fundingInput.txid
val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId))
- val capacity: Satoshi = commitInput.txOut.amount
+ val capacity: Satoshi = fundingAmount
+ // We can safely cast to millisatoshis since we verify that it's less than a valid millisatoshi amount.
+ val maxHtlcValueInFlight: MilliSatoshi = Seq(localCommitParams.maxHtlcValueInFlight, remoteCommitParams.maxHtlcValueInFlight, UInt64(MilliSatoshi.MaxMoney.toLong)).min.toBigInt.toLong.msat
/** Once the funding transaction is confirmed, short_channel_id matching this transaction. */
val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match {
case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId)
case _ => None
}
- def localKeys(params: ChannelParams, channelKeys: ChannelKeys): LocalCommitmentKeys = LocalCommitmentKeys(params, channelKeys, localCommit.index)
+ def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex)
+
+ def commitInput(fundingKey: PrivateKey): InputInfo = Transactions.makeFundingInputInfo(fundingInput.txid, fundingInput.index.toInt, fundingAmount, fundingKey.publicKey, remoteFundingPubKey, commitmentFormat)
+
+ def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys))
- def remoteKeys(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint)
+ def localKeys(params: ChannelParams, channelKeys: ChannelKeys): LocalCommitmentKeys = LocalCommitmentKeys(params, channelKeys, localCommit.index, commitmentFormat)
+
+ def remoteKeys(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint, commitmentFormat)
/** Channel reserve that applies to our funds. */
- def localChannelReserve(params: ChannelParams): Satoshi = params.localChannelReserveForCapacity(capacity, fundingTxIndex > 0)
+ def localChannelReserve(params: ChannelParams): Satoshi = if (params.channelFeatures.hasFeature(Features.DualFunding) || fundingTxIndex > 0) {
+ (fundingAmount / 100).max(remoteCommitParams.dustLimit)
+ } else {
+ params.remoteParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams
+ }
/** Channel reserve that applies to our peer's funds. */
- def remoteChannelReserve(params: ChannelParams): Satoshi = params.remoteChannelReserveForCapacity(capacity, fundingTxIndex > 0)
+ def remoteChannelReserve(params: ChannelParams): Satoshi = if (params.channelFeatures.hasFeature(Features.DualFunding) || fundingTxIndex > 0) {
+ (fundingAmount / 100).max(localCommitParams.dustLimit)
+ } else {
+ params.localParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams
+ }
// NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on
// top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see
@@ -457,11 +454,11 @@ case class Commitment(fundingTxIndex: Long,
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
- val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity)
+ val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, commitmentFormat, capacity)
val remoteFeerate = localCommit.spec.commitTxFeerate +: changes.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
// What we want to avoid is having an HTLC in a commitment transaction that has a very low feerate, which we won't
// be able to confirm in time to claim the HTLC, so we only need to check that the feerate isn't too low.
- remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, feerate)) match {
+ remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(commitmentFormat, localFeerate, feerate)) match {
case Some(feerate) => return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = feerate))
case None =>
}
@@ -474,10 +471,10 @@ case class Commitment(fundingTxIndex: Long,
val outgoingHtlcs = reduced.htlcs.collect(DirectedHtlc.incoming)
// note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment
- val fees = commitTxTotalCost(params.remoteCommitParams.dustLimit, reduced, params.commitmentFormat)
+ val fees = commitTxTotalCost(remoteCommitParams.dustLimit, reduced, commitmentFormat)
// the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid
// getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728).
- val funderFeeBuffer = commitTxTotalCostMsat(params.remoteCommitParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), params.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, params.commitmentFormat)
+ val funderFeeBuffer = commitTxTotalCostMsat(remoteCommitParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat)
// NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold)
// which may result in a lower commit tx fee; this is why we take the max of the two.
val missingForSender = reduced.toRemote - localChannelReserve(params) - (if (params.localParams.paysCommitTxFees) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat)
@@ -508,39 +505,38 @@ case class Commitment(fundingTxIndex: Long,
// We apply local *and* remote restrictions, to ensure both peers are happy with the resulting number of HTLCs.
// NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since outgoingHtlcs is a Set).
val htlcValueInFlight = outgoingHtlcs.toSeq.map(_.amountMsat).sum
- val allowedHtlcValueInFlight = UInt64(params.maxHtlcValueInFlight.toLong)
- if (allowedHtlcValueInFlight < htlcValueInFlight) {
- return Left(HtlcValueTooHighInFlight(params.channelId, maximum = allowedHtlcValueInFlight, actual = htlcValueInFlight))
+ if (maxHtlcValueInFlight < htlcValueInFlight) {
+ return Left(HtlcValueTooHighInFlight(params.channelId, maximum = UInt64(maxHtlcValueInFlight.toLong), actual = htlcValueInFlight))
}
- if (Seq(params.localCommitParams.maxAcceptedHtlcs, params.remoteCommitParams.maxAcceptedHtlcs).min < outgoingHtlcs.size) {
- return Left(TooManyAcceptedHtlcs(params.channelId, maximum = Seq(params.localCommitParams.maxAcceptedHtlcs, params.remoteCommitParams.maxAcceptedHtlcs).min))
+ if (Seq(localCommitParams.maxAcceptedHtlcs, remoteCommitParams.maxAcceptedHtlcs).min < outgoingHtlcs.size) {
+ return Left(TooManyAcceptedHtlcs(params.channelId, maximum = Seq(localCommitParams.maxAcceptedHtlcs, remoteCommitParams.maxAcceptedHtlcs).min))
}
// If sending this htlc would overflow our dust exposure, we reject it.
val maxDustExposure = feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.maxExposure
val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all)
- val localDustExposureAfterAdd = DustExposure.computeExposure(localReduced, params.localCommitParams.dustLimit, params.commitmentFormat)
+ val localDustExposureAfterAdd = DustExposure.computeExposure(localReduced, localCommitParams.dustLimit, commitmentFormat)
if (localDustExposureAfterAdd > maxDustExposure) {
return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterAdd))
}
val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit1.spec, changes.remoteChanges.all, changes.localChanges.all)
- val remoteDustExposureAfterAdd = DustExposure.computeExposure(remoteReduced, params.remoteCommitParams.dustLimit, params.commitmentFormat)
+ val remoteDustExposureAfterAdd = DustExposure.computeExposure(remoteReduced, remoteCommitParams.dustLimit, commitmentFormat)
if (remoteDustExposureAfterAdd > maxDustExposure) {
return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterAdd))
}
// Jamming protection
// Must be the last checks so that they can be ignored for shadow deployment.
- reputationScore.checkOutgoingChannelOccupancy(outgoingHtlcs.toSeq, params)
+ reputationScore.checkOutgoingChannelOccupancy(params.channelId, this, outgoingHtlcs.toSeq)
}
def canReceiveAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = {
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
- val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity)
+ val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, commitmentFormat, capacity)
val remoteFeerate = localCommit.spec.commitTxFeerate +: changes.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
- remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, feerate)) match {
+ remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(commitmentFormat, localFeerate, feerate)) match {
case Some(feerate) => return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = feerate))
case None =>
}
@@ -550,7 +546,7 @@ case class Commitment(fundingTxIndex: Long,
val incomingHtlcs = reduced.htlcs.collect(DirectedHtlc.incoming)
// note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment
- val fees = commitTxTotalCost(params.localCommitParams.dustLimit, reduced, params.commitmentFormat)
+ val fees = commitTxTotalCost(localCommitParams.dustLimit, reduced, commitmentFormat)
// NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place
// We could enforce it once we're confident a large portion of the network implements it.
val missingForSender = reduced.toRemote - remoteChannelReserve(params) - (if (params.localParams.paysCommitTxFees) 0.sat else fees)
@@ -570,12 +566,12 @@ case class Commitment(fundingTxIndex: Long,
// NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since incomingHtlcs is a Set).
val htlcValueInFlight = incomingHtlcs.toSeq.map(_.amountMsat).sum
- if (params.localCommitParams.maxHtlcValueInFlight < htlcValueInFlight) {
- return Left(HtlcValueTooHighInFlight(params.channelId, maximum = params.localCommitParams.maxHtlcValueInFlight, actual = htlcValueInFlight))
+ if (localCommitParams.maxHtlcValueInFlight < htlcValueInFlight) {
+ return Left(HtlcValueTooHighInFlight(params.channelId, maximum = localCommitParams.maxHtlcValueInFlight, actual = htlcValueInFlight))
}
- if (incomingHtlcs.size > params.localCommitParams.maxAcceptedHtlcs) {
- return Left(TooManyAcceptedHtlcs(params.channelId, maximum = params.localCommitParams.maxAcceptedHtlcs))
+ if (incomingHtlcs.size > localCommitParams.maxAcceptedHtlcs) {
+ return Left(TooManyAcceptedHtlcs(params.channelId, maximum = localCommitParams.maxAcceptedHtlcs))
}
Right(())
@@ -586,7 +582,7 @@ case class Commitment(fundingTxIndex: Long,
val reduced = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed)
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
// we look from remote's point of view, so if local is initiator remote doesn't pay the fees
- val fees = commitTxTotalCost(params.remoteCommitParams.dustLimit, reduced, params.commitmentFormat)
+ val fees = commitTxTotalCost(remoteCommitParams.dustLimit, reduced, commitmentFormat)
val missing = reduced.toRemote.truncateToSatoshi - localChannelReserve(params) - fees
if (missing < 0.sat) {
return Left(CannotAffordFees(params.channelId, missing = -missing, reserve = localChannelReserve(params), fees = fees))
@@ -597,12 +593,12 @@ case class Commitment(fundingTxIndex: Long,
// this is the commitment as it would be if our update_fee was immediately signed by both parties (it is only an
// estimate because there can be concurrent updates)
val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all)
- val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, params.localCommitParams.dustLimit, params.commitmentFormat)
+ val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, localCommitParams.dustLimit, commitmentFormat)
if (localDustExposureAfterFeeUpdate > maxDustExposure) {
return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterFeeUpdate))
}
val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit.spec, changes.remoteChanges.all, changes.localChanges.all)
- val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, params.remoteCommitParams.dustLimit, params.commitmentFormat)
+ val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, remoteCommitParams.dustLimit, commitmentFormat)
if (remoteDustExposureAfterFeeUpdate > maxDustExposure) {
return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterFeeUpdate))
}
@@ -611,10 +607,10 @@ case class Commitment(fundingTxIndex: Long,
}
def canReceiveFee(targetFeerate: FeeratePerKw, params: ChannelParams, changes: CommitmentChanges, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = {
- val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity)
- if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooHigh(params.commitmentFormat, localFeerate, targetFeerate)) {
+ val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, commitmentFormat, capacity)
+ if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooHigh(commitmentFormat, localFeerate, targetFeerate)) {
return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = targetFeerate))
- } else if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, targetFeerate) && hasPendingOrProposedHtlcs(changes)) {
+ } else if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(commitmentFormat, localFeerate, targetFeerate) && hasPendingOrProposedHtlcs(changes)) {
// If the proposed feerate is too low, but we don't have any pending HTLC, we temporarily accept it.
return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = targetFeerate))
} else {
@@ -625,7 +621,7 @@ case class Commitment(fundingTxIndex: Long,
// (it also means that we need to check the fee of the initial commitment tx somewhere)
val reduced = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed)
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
- val fees = commitTxTotalCost(params.localCommitParams.dustLimit, reduced, params.commitmentFormat)
+ val fees = commitTxTotalCost(localCommitParams.dustLimit, reduced, commitmentFormat)
val missing = reduced.toRemote.truncateToSatoshi - remoteChannelReserve(params) - fees
if (missing < 0.sat) {
return Left(CannotAffordFees(params.channelId, missing = -missing, reserve = remoteChannelReserve(params), fees = fees))
@@ -634,14 +630,14 @@ case class Commitment(fundingTxIndex: Long,
if (feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.closeOnUpdateFeeOverflow) {
val maxDustExposure = feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.maxExposure
val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all)
- val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, params.localCommitParams.dustLimit, params.commitmentFormat)
+ val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, localCommitParams.dustLimit, commitmentFormat)
if (localDustExposureAfterFeeUpdate > maxDustExposure) {
return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterFeeUpdate))
}
// this is the commitment as it would be if their update_fee was immediately signed by both parties (it is only an
// estimate because there can be concurrent updates)
val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit.spec, changes.remoteChanges.all, changes.localChanges.all)
- val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, params.remoteCommitParams.dustLimit, params.commitmentFormat)
+ val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, remoteCommitParams.dustLimit, commitmentFormat)
if (remoteDustExposureAfterFeeUpdate > maxDustExposure) {
return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterFeeUpdate))
}
@@ -653,8 +649,8 @@ case class Commitment(fundingTxIndex: Long,
def sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int)(implicit log: LoggingAdapter): (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 = channelKeys.fundingKey(fundingTxIndex)
- val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, params.remoteCommitParams, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput, spec)
+ 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
@@ -664,7 +660,7 @@ case class Commitment(fundingTxIndex: Long,
val tlvs = Set(
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None
).flatten[CommitSigTlv]
- val commitSig = params.commitmentFormat match {
+ val commitSig = commitmentFormat match {
case _: SegwitV0CommitmentFormat =>
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig
CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs))
@@ -684,9 +680,9 @@ case class Commitment(fundingTxIndex: Long,
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
// and will increment our index
val localCommitIndex = localCommit.index + 1
- val fundingKey = channelKeys.fundingKey(fundingTxIndex)
+ val fundingKey = localFundingKey(channelKeys)
val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed)
- LocalCommit.fromCommitSig(params, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec).map { localCommit1 =>
+ LocalCommit.fromCommitSig(params, localCommitParams, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commit, localCommitIndex, spec, commitmentFormat).map { localCommit1 =>
log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.txId} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","))
copy(localCommit = localCommit1)
}
@@ -694,9 +690,9 @@ case class Commitment(fundingTxIndex: Long,
/** Return a fully signed commit tx, that can be published as-is. */
def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = {
- val fundingKey = channelKeys.fundingKey(fundingTxIndex)
+ val fundingKey = localFundingKey(channelKeys)
val commitKeys = localKeys(params, channelKeys)
- val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, params.localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec)
+ val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec)
localCommit.remoteSig match {
case remoteSig: ChannelSpendSignature.IndividualSignature =>
val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey)
@@ -707,14 +703,14 @@ case class Commitment(fundingTxIndex: Long,
/** Return the HTLC transactions for our local commit and the corresponding remote signatures. */
def htlcTxs(params: ChannelParams, channelKeys: ChannelKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = {
- val fundingKey = channelKeys.fundingKey(fundingTxIndex)
+ val fundingKey = localFundingKey(channelKeys)
val commitKeys = localKeys(params, channelKeys)
htlcTxs(params, fundingKey, commitKeys)
}
/** Return the HTLC transactions for our local commit and the corresponding remote signatures. */
def htlcTxs(params: ChannelParams, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = {
- val (_, htlcTxs) = Commitment.makeLocalTxs(params, params.localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec)
+ val (_, htlcTxs) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec)
htlcTxs.sortBy(_.input.outPoint.index).zip(localCommit.htlcRemoteSigs)
}
@@ -728,10 +724,11 @@ object Commitment {
localFundingKey: PrivateKey,
remoteFundingPubKey: PublicKey,
commitmentInput: InputInfo,
+ commitmentFormat: CommitmentFormat,
spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = {
- val outputs = makeCommitTxOutputs(localFundingKey.publicKey, remoteFundingPubKey, commitKeys.publicKeys, channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, channelParams.commitmentFormat)
+ val outputs = makeCommitTxOutputs(localFundingKey.publicKey, remoteFundingPubKey, commitKeys.publicKeys, channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs)
- val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, channelParams.commitmentFormat)
+ val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
(commitTx, htlcTxs)
}
@@ -742,10 +739,11 @@ object Commitment {
localFundingKey: PrivateKey,
remoteFundingPubKey: PublicKey,
commitmentInput: InputInfo,
+ commitmentFormat: CommitmentFormat,
spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = {
- val outputs = makeCommitTxOutputs(remoteFundingPubKey, localFundingKey.publicKey, commitKeys.publicKeys, !channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, channelParams.commitmentFormat)
+ val outputs = makeCommitTxOutputs(remoteFundingPubKey, localFundingKey.publicKey, commitKeys.publicKeys, !channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs)
- val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, channelParams.commitmentFormat)
+ val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
(commitTx, htlcTxs)
}
}
@@ -758,32 +756,31 @@ case class AnnouncedCommitment(commitment: Commitment, announcement: ChannelAnno
}
/** Subset of Commitments when we want to work with a single, specific commitment. */
-case class FullCommitment(channelParams: ChannelParams, changes: CommitmentChanges,
- fundingTxIndex: Long,
- firstRemoteCommitIndex: Long,
- remoteFundingPubKey: PublicKey,
- localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus,
- localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) {
+case class FullCommitment(channelParams: ChannelParams, changes: CommitmentChanges, commitment: Commitment) {
val channelId: ByteVector32 = channelParams.channelId
- val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match {
- case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId)
- case _ => None
- }
+ val shortChannelId_opt: Option[RealShortChannelId] = commitment.shortChannelId_opt
+ val fundingTxIndex: Long = commitment.fundingTxIndex
+ val fundingInput: OutPoint = commitment.fundingInput
+ val fundingTxId: TxId = commitment.fundingTxId
+ val remoteFundingPubKey: PublicKey = commitment.remoteFundingPubKey
+ val localFundingStatus: LocalFundingStatus = commitment.localFundingStatus
+ val commitTxIds: CommitTxIds = commitment.commitTxIds
val localChannelParams: LocalChannelParams = channelParams.localParams
- val localCommitParams: CommitParams = channelParams.localCommitParams
+ val localCommitParams: CommitParams = commitment.localCommitParams
+ val localCommit: LocalCommit = commitment.localCommit
val remoteChannelParams: RemoteChannelParams = channelParams.remoteParams
- val remoteCommitParams: CommitParams = channelParams.remoteCommitParams
- val commitInput: InputInfo = localCommit.input
- val fundingTxId: TxId = commitInput.outPoint.txid
- val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId))
- val capacity: Satoshi = commitInput.txOut.amount
- val commitmentFormat: CommitmentFormat = channelParams.commitmentFormat
- val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, nextRemoteCommit_opt)
+ val remoteCommitParams: CommitParams = commitment.remoteCommitParams
+ val remoteCommit: RemoteCommit = commitment.remoteCommit
+ val nextRemoteCommit_opt: Option[NextRemoteCommit] = commitment.nextRemoteCommit_opt
+ val commitmentFormat: CommitmentFormat = commitment.commitmentFormat
+ val capacity: Satoshi = commitment.fundingAmount
def localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = commitment.localKeys(channelParams, channelKeys)
def remoteKeys(channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = commitment.remoteKeys(channelParams, channelKeys, remotePerCommitmentPoint)
+ def commitInput(channelKeys: ChannelKeys): InputInfo = commitment.commitInput(channelKeys)
+
def localChannelReserve: Satoshi = commitment.localChannelReserve(channelParams)
def remoteChannelReserve: Satoshi = commitment.remoteChannelReserve(channelParams)
@@ -851,13 +848,14 @@ case class Commitments(channelParams: ChannelParams,
// While we have multiple active commitments, we use the most restrictive one.
val capacity: Satoshi = active.map(_.capacity).min
+ val maxHtlcValueInFlight: MilliSatoshi = active.map(_.maxHtlcValueInFlight).min
lazy val availableBalanceForSend: MilliSatoshi = active.map(_.availableBalanceForSend(channelParams, changes)).min
lazy val availableBalanceForReceive: MilliSatoshi = active.map(_.availableBalanceForReceive(channelParams, changes)).min
val all: Seq[Commitment] = active ++ inactive
// We always use the last commitment that was created, to make sure we never go back in time.
- val latest: FullCommitment = FullCommitment(channelParams, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.remoteFundingPubKey, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt)
+ val latest: FullCommitment = FullCommitment(channelParams, changes, active.head)
val lastLocalLocked_opt: Option[Commitment] = active.filter(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).sortBy(_.fundingTxIndex).lastOption
val lastRemoteLocked_opt: Option[Commitment] = active.filter(c => c.remoteFundingStatus == RemoteFundingStatus.Locked).sortBy(_.fundingTxIndex).lastOption
@@ -897,7 +895,7 @@ case class Commitments(channelParams: ChannelParams,
}
// even if remote advertises support for 0 msat htlc, we limit ourselves to values strictly positive, hence the max(1 msat)
- val htlcMinimum = channelParams.remoteCommitParams.htlcMinimum.max(1 msat)
+ val htlcMinimum = active.map(_.remoteCommitParams.htlcMinimum).max.max(1 msat)
if (cmd.amount < htlcMinimum) {
return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = cmd.amount))
}
@@ -939,7 +937,7 @@ case class Commitments(channelParams: ChannelParams,
}
// we used to not enforce a strictly positive minimum, hence the max(1 msat)
- val htlcMinimum = channelParams.localCommitParams.htlcMinimum.max(1 msat)
+ val htlcMinimum = active.map(_.localCommitParams.htlcMinimum).max.max(1 msat)
if (add.amountMsat < htlcMinimum) {
return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = add.amountMsat))
}
@@ -1038,7 +1036,7 @@ case class Commitments(channelParams: ChannelParams,
active.map(_.canSendFee(cmd.feeratePerKw, channelParams, changes1, feeConf))
.collectFirst { case Left(f) => Left(f) }
.getOrElse {
- Metrics.LocalFeeratePerByte.withTag(Tags.CommitmentFormat, channelParams.commitmentFormat.toString).record(FeeratePerByte(cmd.feeratePerKw).feerate.toLong)
+ Metrics.LocalFeeratePerByte.withTag(Tags.CommitmentFormat, active.head.commitmentFormat.toString).record(FeeratePerByte(cmd.feeratePerKw).feerate.toLong)
Right(copy(changes = changes1), fee)
}
}
@@ -1050,14 +1048,14 @@ case class Commitments(channelParams: ChannelParams,
} else if (fee.feeratePerKw < FeeratePerKw.MinimumFeeratePerKw) {
Left(FeerateTooSmall(channelId, remoteFeeratePerKw = fee.feeratePerKw))
} else {
- val localFeeratePerKw = feeConf.getCommitmentFeerate(feerates, remoteNodeId, channelParams.commitmentFormat, active.head.capacity)
+ val localFeeratePerKw = feeConf.getCommitmentFeerate(feerates, remoteNodeId, active.head.commitmentFormat, active.head.capacity)
log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toLong.toDouble / localFeeratePerKw.toLong)
// update_fee replace each other, so we can remove previous ones
val changes1 = changes.copy(remoteChanges = changes.remoteChanges.copy(proposed = changes.remoteChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee))
active.map(_.canReceiveFee(fee.feeratePerKw, channelParams, changes1, feerates, feeConf))
.collectFirst { case Left(f) => Left(f) }
.getOrElse {
- Metrics.RemoteFeeratePerByte.withTag(Tags.CommitmentFormat, channelParams.commitmentFormat.toString).record(FeeratePerByte(fee.feeratePerKw).feerate.toLong)
+ Metrics.RemoteFeeratePerByte.withTag(Tags.CommitmentFormat, active.head.commitmentFormat.toString).record(FeeratePerByte(fee.feeratePerKw).feerate.toLong)
Right(copy(changes = changes1))
}
}
@@ -1067,8 +1065,10 @@ case class Commitments(channelParams: ChannelParams,
remoteNextCommitInfo match {
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
case Right(remoteNextPerCommitmentPoint) =>
- val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remoteNextPerCommitmentPoint)
- val (active1, sigs) = active.map(_.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size)).unzip
+ val (active1, sigs) = active.map(c => {
+ val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remoteNextPerCommitmentPoint, c.commitmentFormat)
+ c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size)
+ }).unzip
val commitments1 = copy(
changes = changes.copy(
localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed),
@@ -1092,9 +1092,9 @@ case class Commitments(channelParams: ChannelParams,
case _: CommitSig if active.size > 1 => return Left(CommitSigCountMismatch(channelId, active.size, 1))
case commitSig: CommitSig => Seq(commitSig)
}
- val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex + 1)
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
val active1 = active.zip(sigs).map { case (commitment, commit) =>
+ val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex + 1, commitment.commitmentFormat)
commitment.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit) match {
case Left(f) => return Left(f)
case Right(commitment1) => commitment1
@@ -1159,21 +1159,21 @@ case class Commitments(channelParams: ChannelParams,
case _ => true
})
val localReduced = DustExposure.reduceForDustExposure(localSpecWithoutNewHtlcs, changes.localChanges.all, changes.remoteChanges.acked)
- val localCommitDustExposure = DustExposure.computeExposure(localReduced, channelParams.localCommitParams.dustLimit, channelParams.commitmentFormat)
+ val localCommitDustExposure = active.map(c => DustExposure.computeExposure(localReduced, c.localCommitParams.dustLimit, c.commitmentFormat)).max
val remoteReduced = DustExposure.reduceForDustExposure(remoteSpecWithoutNewHtlcs, changes.remoteChanges.acked, changes.localChanges.all)
- val remoteCommitDustExposure = DustExposure.computeExposure(remoteReduced, channelParams.remoteCommitParams.dustLimit, channelParams.commitmentFormat)
+ val remoteCommitDustExposure = active.map(c => DustExposure.computeExposure(remoteReduced, c.remoteCommitParams.dustLimit, c.commitmentFormat)).max
// we sort incoming htlcs by decreasing amount: we want to prioritize higher amounts.
val sortedReceivedHtlcs = receivedHtlcs.sortBy(_.amountMsat).reverse
DustExposure.filterBeforeForward(
maxDustExposure,
localReduced,
- channelParams.localCommitParams.dustLimit,
+ active.map(_.localCommitParams.dustLimit).max,
localCommitDustExposure,
remoteReduced,
- channelParams.remoteCommitParams.dustLimit,
+ active.map(_.remoteCommitParams.dustLimit).max,
remoteCommitDustExposure,
sortedReceivedHtlcs,
- channelParams.commitmentFormat)
+ active.head.commitmentFormat)
}
val actions = acceptedHtlcs.map(add => PostRevocationAction.RelayHtlc(add)) ++
rejectedHtlcs.map(add => PostRevocationAction.RejectHtlc(add)) ++
@@ -1212,10 +1212,14 @@ case class Commitments(channelParams: ChannelParams,
def validateSeed(channelKeys: ChannelKeys): Boolean = {
active.forall { commitment =>
- val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey
- val remoteFundingKey = commitment.remoteFundingPubKey
- val redeemInfo = Helpers.Funding.makeFundingScript(localFundingKey, remoteFundingKey, channelParams.commitmentFormat)
- commitment.commitInput.txOut.publicKeyScript == redeemInfo.pubkeyScript
+ commitment.localFundingStatus match {
+ // We ignore unconfirmed transactions for simplicity.
+ case _: LocalFundingStatus.UnconfirmedFundingTx => true
+ case tx: LocalFundingStatus.ConfirmedFundingTx =>
+ val localFundingKey = commitment.localFundingKey(channelKeys).publicKey
+ val redeemInfo = Transactions.makeFundingScript(localFundingKey, commitment.remoteFundingPubKey, commitment.commitmentFormat)
+ tx.txOut.publicKeyScript == redeemInfo.pubkeyScript
+ }
}
}
@@ -1353,7 +1357,7 @@ case class Commitments(channelParams: ChannelParams,
* @param spendingTx A transaction that may spend a current or former funding tx
*/
def resolveCommitment(spendingTx: Transaction): Option[Commitment] = {
- all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.commitInput.outPoint))
+ all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.fundingInput))
}
/** Find the corresponding commitment based on its short_channel_id (once funding transaction is confirmed). */
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 2ee5f15..23e6ba4 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
@@ -30,7 +30,6 @@ import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.DirectedHtlc._
-import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.protocol._
@@ -134,8 +133,8 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
// 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, channelFeatures.commitmentFormat, open.fundingSatoshis)
- if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
+ val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, open.fundingSatoshis)
+ if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
// we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment
// now, but it will be done later when we receive `funding_created`
@@ -181,8 +180,8 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
// 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, channelFeatures.commitmentFormat, open.fundingAmount)
- if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
+ val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, open.fundingAmount)
+ if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
for {
script_opt <- extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt)
@@ -273,7 +272,7 @@ object Helpers {
for {
script_opt <- extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt)
- fundingScript = Funding.makeFundingScript(open.fundingPubkey, accept.fundingPubkey, channelType.commitmentFormat).pubkeyScript
+ fundingScript = Transactions.makeFundingScript(open.fundingPubkey, accept.fundingPubkey, channelType.commitmentFormat).pubkeyScript
liquidityPurchase_opt <- LiquidityAds.validateRemoteFunding(open.requestFunding_opt, remoteNodeId, accept.temporaryChannelId, fundingScript, accept.fundingAmount, open.fundingFeerate, isChannelCreation = true, accept.willFund_opt)
} yield {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
@@ -368,17 +367,17 @@ object Helpers {
def maxHtlcAmount(nodeParams: NodeParams, commitments: Commitments): MilliSatoshi = {
if (!commitments.announceChannel) {
// The channel is private, let's not change the channel update needlessly.
- return commitments.channelParams.maxHtlcValueInFlight
+ return commitments.maxHtlcValueInFlight
}
for (balanceThreshold <- nodeParams.channelConf.balanceThresholds) {
if (commitments.availableBalanceForSend <= balanceThreshold.available) {
// Our maximum HTLC amount must always be greater than htlc_minimum_msat.
- val allowedHtlcAmount = Seq(balanceThreshold.maxHtlcAmount.toMilliSatoshi, commitments.channelParams.localCommitParams.htlcMinimum, commitments.channelParams.remoteCommitParams.htlcMinimum).max
+ val allowedHtlcAmount = Seq(balanceThreshold.maxHtlcAmount.toMilliSatoshi, commitments.latest.localCommitParams.htlcMinimum, commitments.latest.remoteCommitParams.htlcMinimum).max
// But it cannot exceed the channel's max_htlc_value_in_flight_msat.
- return allowedHtlcAmount.min(commitments.channelParams.maxHtlcValueInFlight)
+ return allowedHtlcAmount.min(commitments.maxHtlcValueInFlight)
}
}
- commitments.channelParams.maxHtlcValueInFlight
+ commitments.maxHtlcValueInFlight
}
def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, announceChannel: Boolean): RelayFees = {
@@ -388,37 +387,26 @@ object Helpers {
object Funding {
- def makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo = {
- commitmentFormat match {
- case _: SegwitV0CommitmentFormat => RedeemInfo.P2wsh(Script.write(multiSig2of2(localFundingKey, remoteFundingKey)))
- case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(Taproot.musig2Aggregate(localFundingKey, remoteFundingKey), None)
- }
- }
-
- def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = {
- val redeemInfo = makeFundingScript(fundingPubkey1, fundingPubkey2, commitmentFormat)
- val fundingTxOut = TxOut(fundingSatoshis, redeemInfo.pubkeyScript)
- InputInfo(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, ByteVector.empty)
- }
-
/**
* Creates both sides' first commitment transaction.
*
* @return (localSpec, localTx, remoteSpec, remoteTx)
*/
- def makeFirstCommitTxs(params: ChannelParams,
+ def makeFirstCommitTxs(channelParams: ChannelParams,
+ localCommitParams: CommitParams, remoteCommitParams: CommitParams,
localFundingAmount: Satoshi, remoteFundingAmount: Satoshi,
localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi,
- commitTxFeerate: FeeratePerKw,
+ commitTxFeerate: FeeratePerKw, commitmentFormat: CommitmentFormat,
fundingTxId: TxId, fundingTxOutputIndex: Int,
localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey,
localCommitKeys: LocalCommitmentKeys, remoteCommitKeys: RemoteCommitmentKeys): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
- makeCommitTxs(params,
+ makeCommitTxs(channelParams, localCommitParams, remoteCommitParams,
fundingAmount = localFundingAmount + remoteFundingAmount,
toLocal = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount,
toRemote = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount,
localHtlcs = Set.empty,
commitTxFeerate,
+ commitmentFormat,
fundingTxIndex = 0,
fundingTxId, fundingTxOutputIndex,
localFundingKey, remoteFundingPubKey,
@@ -432,11 +420,14 @@ object Helpers {
* This creates commitment transactions for both sides at an arbitrary `commitmentIndex` and with (optional) `htlc`
* outputs. This function should only be used when commitments are synchronized (local and remote htlcs match).
*/
- def makeCommitTxs(params: ChannelParams,
+ def makeCommitTxs(channelParams: ChannelParams,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
fundingAmount: Satoshi,
toLocal: MilliSatoshi, toRemote: MilliSatoshi,
localHtlcs: Set[DirectedHtlc],
commitTxFeerate: FeeratePerKw,
+ commitmentFormat: CommitmentFormat,
fundingTxIndex: Long,
fundingTxId: TxId, fundingTxOutputIndex: Int,
localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey,
@@ -445,21 +436,21 @@ object Helpers {
val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote)
val remoteSpec = CommitmentSpec(localHtlcs.map(_.opposite), commitTxFeerate, toLocal = toRemote, toRemote = toLocal)
- if (!params.localParams.paysCommitTxFees) {
+ if (!channelParams.localParams.paysCommitTxFees) {
// They are responsible for paying the commitment transaction fee: we need to make sure they can afford it!
// Note that the reserve may not always be met: we could be using dual funding with a large funding amount on
// our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for
// the commitment transaction.
- val fees = commitTxTotalCost(params.remoteCommitParams.dustLimit, remoteSpec, params.commitmentFormat)
+ val fees = commitTxTotalCost(remoteCommitParams.dustLimit, remoteSpec, commitmentFormat)
val missing = fees - toRemote.truncateToSatoshi
if (missing > 0.sat) {
- return Left(CannotAffordFirstCommitFees(params.channelId, missing = missing, fees = fees))
+ return Left(CannotAffordFirstCommitFees(channelParams.channelId, missing = missing, fees = fees))
}
}
- val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey, remoteFundingPubKey, params.commitmentFormat)
- val (localCommitTx, _) = Commitment.makeLocalTxs(params, params.localCommitParams, localCommitKeys, localCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, localSpec)
- val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, params.remoteCommitParams, remoteCommitKeys, remoteCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, remoteSpec)
+ val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey, remoteFundingPubKey, commitmentFormat)
+ val (localCommitTx, _) = Commitment.makeLocalTxs(channelParams, localCommitParams, localCommitKeys, localCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, commitmentFormat, localSpec)
+ val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(channelParams, remoteCommitParams, remoteCommitKeys, remoteCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, commitmentFormat, remoteSpec)
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
Right(localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)
}
@@ -679,9 +670,9 @@ object Helpers {
}
}
- def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = {
+ def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = {
// this is just to estimate the weight, it depends on size of the pubkey scripts
- val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, 0 sat, 0 sat, commitment.localCommit.spec)
+ 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 closingWeight = dummyClosingTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig).weight()
@@ -689,7 +680,7 @@ object Helpers {
feerates.computeFees(closingWeight)
}
- def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = {
+ def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = {
val requestedFeerate = onChainFeeConf.getClosingFeerate(feerates)
val preferredFeerate = commitment.commitmentFormat match {
case DefaultCommitmentFormat =>
@@ -700,15 +691,15 @@ object Helpers {
// NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can
// always use CPFP to speed up confirmation if necessary.
val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(ConfirmationPriority.Slow.getFeerate(feerates)), preferredFeerate * 2)
- firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates)
+ firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates)
}
def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
def makeFirstClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
val closingFees = closingFeerates_opt match {
- case Some(closingFeerates) => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates)
- case None => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf)
+ case Some(closingFeerates) => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates)
+ case None => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf)
}
makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees)
}
@@ -716,7 +707,7 @@ object Helpers {
def makeClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, commitment.specs2String)
val dustLimit = commitment.localCommitParams.dustLimit.max(commitment.remoteCommitParams.dustLimit)
- val closingTx = ClosingTx.createUnsignedTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec)
+ val closingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec)
val localClosingSig = closingTx.sign(channelKeys.fundingKey(commitment.fundingTxIndex), commitment.remoteFundingPubKey).sig
val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max)))
log.debug(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}")
@@ -742,8 +733,9 @@ 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)] = {
// 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(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
+ 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
@@ -754,7 +746,7 @@ object Helpers {
}
}
// Now that we know the fee we're ready to pay, we can create our closing transactions.
- val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
+ val closingTxs = Transactions.makeSimpleClosingTxs(commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
closingTxs.preferred_opt match {
case Some(closingTx) if closingTx.fee > 0.sat => ()
case _ => return Left(CannotGenerateClosingTx(commitment.channelId))
@@ -776,7 +768,7 @@ object Helpers {
*/
def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
- val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey)
+ 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 {
@@ -1087,7 +1079,7 @@ object Helpers {
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit)
- val mainTx_opt = claimMainOutput(commitment.channelParams, commitKeys, commitTx, feerates, onChainFeeConf, finalScriptPubKey)
+ val mainTx_opt = claimMainOutput(commitKeys, commitTx, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, feerates, onChainFeeConf, finalScriptPubKey)
val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey)
val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey)
val anchorOutput_opt = ClaimRemoteAnchorTx.findInput(commitTx, fundingKey, commitKeys, commitment.commitmentFormat).toOption
@@ -1116,14 +1108,14 @@ object Helpers {
}
/** Claim our main output from the remote commitment transaction, if available. */
- def claimMainOutput(params: ChannelParams, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = {
+ def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = {
val feerate = onChainFeeConf.getClosingFeerate(feerates)
- params.commitmentFormat match {
+ commitmentFormat match {
case DefaultCommitmentFormat => withTxGenerationLog("remote-main") {
- ClaimP2WPKHOutputTx.createUnsignedTx(commitKeys, commitTx, params.localCommitParams.dustLimit, finalScriptPubKey, feerate, params.commitmentFormat)
+ ClaimP2WPKHOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
}
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
- ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, params.localCommitParams.dustLimit, finalScriptPubKey, feerate, params.commitmentFormat)
+ ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
}
}
}
@@ -1278,11 +1270,10 @@ object Helpers {
* When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that
* will punish our peer by stealing all their funds.
*/
- def claimCommitTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, db: ChannelsDb, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, SecondStageTransactions) = {
- import params._
+ def claimCommitTxOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, db: ChannelsDb, dustLimit: Satoshi, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, SecondStageTransactions) = {
log.warning("a revoked commit has been published with commitmentNumber={}", commitmentNumber)
- val commitKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentSecret.publicKey)
+ val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey, commitmentFormat)
val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret)
val feerateMain = onChainFeeConf.getClosingFeerate(feerates)
@@ -1292,23 +1283,23 @@ object Helpers {
// First we will claim our main output right away.
val mainTx_opt = commitmentFormat match {
case DefaultCommitmentFormat => withTxGenerationLog("remote-main") {
- ClaimP2WPKHOutputTx.createUnsignedTx(commitKeys, commitTx, localCommitParams.dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
+ ClaimP2WPKHOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
}
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
- ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, localCommitParams.dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
+ ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
}
}
// Then we punish them by stealing their main output.
val mainPenaltyTx_opt = withTxGenerationLog("main-penalty") {
- MainPenaltyTx.createUnsignedTx(commitKeys, revocationKey, commitTx, localCommitParams.dustLimit, finalScriptPubKey, remoteCommitParams.toSelfDelay, feeratePenalty, commitmentFormat)
+ MainPenaltyTx.createUnsignedTx(commitKeys, revocationKey, commitTx, dustLimit, finalScriptPubKey, toSelfDelay, feeratePenalty, commitmentFormat)
}
// We retrieve the historical information needed to rebuild htlc scripts.
- val htlcInfos = db.listHtlcInfos(channelId, commitmentNumber)
+ val htlcInfos = db.listHtlcInfos(channelParams.channelId, commitmentNumber)
log.info("got {} htlcs for commitmentNumber={}", htlcInfos.size, commitmentNumber)
// And finally we steal the htlc outputs.
- val htlcPenaltyTxs = HtlcPenaltyTx.createUnsignedTxs(commitKeys, revocationKey, commitTx, htlcInfos, localCommitParams.dustLimit, finalScriptPubKey, feeratePenalty, commitmentFormat)
+ val htlcPenaltyTxs = HtlcPenaltyTx.createUnsignedTxs(commitKeys, revocationKey, commitTx, htlcInfos, dustLimit, finalScriptPubKey, feeratePenalty, commitmentFormat)
.flatMap(htlcPenaltyTx => withTxGenerationLog("htlc-penalty")(htlcPenaltyTx))
val rvk = RevokedCommitPublished(
@@ -1336,18 +1327,18 @@ object Helpers {
* NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same
* lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs.
*/
- def claimHtlcTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, ThirdStageTransactions) = {
+ def claimHtlcTxOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, ThirdStageTransactions) = {
// We published HTLC-penalty transactions for every HTLC output: this transaction may be ours, or it may be one
// of their HTLC transactions that confirmed before our HTLC-penalty transaction. If it is spending an HTLC
// output, we assume that it's an HTLC transaction published by our peer and try to create penalty transactions
// that spend it, which will automatically be skipped if this was instead one of our HTLC-penalty transactions.
val spendsHtlcOutput = htlcTx.txIn.exists(txIn => revokedCommitPublished.htlcOutputs.contains(txIn.outPoint))
if (spendsHtlcOutput) {
- getRemotePerCommitmentSecret(params, channelKeys, remotePerCommitmentSecrets, revokedCommitPublished.commitTx).map {
+ getRemotePerCommitmentSecret(channelParams, channelKeys, remotePerCommitmentSecrets, revokedCommitPublished.commitTx).map {
case (_, remotePerCommitmentSecret) =>
- val commitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentSecret.publicKey)
+ val commitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey, commitmentFormat)
val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret)
- val penaltyTxs = claimHtlcTxOutputs(params, commitmentKeys, revocationKey, htlcTx, feerates, finalScriptPubKey)
+ val penaltyTxs = claimHtlcTxOutputs(commitmentKeys, revocationKey, toSelfDelay, commitmentFormat, htlcTx, dustLimit, feerates, finalScriptPubKey)
val revokedCommitPublished1 = revokedCommitPublished.copy(htlcDelayedOutputs = revokedCommitPublished.htlcDelayedOutputs ++ penaltyTxs.map(_.input.outPoint))
val txs = ThirdStageTransactions(penaltyTxs)
(revokedCommitPublished1, txs)
@@ -1357,12 +1348,10 @@ object Helpers {
}
}
- private def claimHtlcTxOutputs(params: ChannelParams, commitmentKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[ClaimHtlcDelayedOutputPenaltyTx] = {
+ private def claimHtlcTxOutputs(commitmentKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, htlcTx: Transaction, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[ClaimHtlcDelayedOutputPenaltyTx] = {
// We need to use a high fee when spending HTLC txs because after a delay they can also be spent by the counterparty.
val feeratePenalty = feerates.fastest
- val dustLimit = params.localCommitParams.dustLimit
- val toSelfDelay = params.remoteCommitParams.toSelfDelay
- ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(commitmentKeys, revocationKey, htlcTx, dustLimit, toSelfDelay, finalScriptPubKey, feeratePenalty, params.commitmentFormat).flatMap(penaltyTx => {
+ ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(commitmentKeys, revocationKey, htlcTx, dustLimit, toSelfDelay, finalScriptPubKey, feeratePenalty, commitmentFormat).flatMap(penaltyTx => {
withTxGenerationLog("htlc-delayed-penalty")(penaltyTx)
})
}
@@ -1370,11 +1359,11 @@ object Helpers {
/**
* Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed.
*/
- def claimHtlcTxsOutputs(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecret: PrivateKey, revokedCommitPublished: RevokedCommitPublished, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = {
- val commitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentSecret.publicKey)
+ def claimHtlcTxsOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecret: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, revokedCommitPublished: RevokedCommitPublished, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = {
+ val commitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey, commitmentFormat)
val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret)
val confirmedHtlcTxs = revokedCommitPublished.htlcOutputs.flatMap(htlcOutput => revokedCommitPublished.irrevocablySpent.get(htlcOutput))
- val penaltyTxs = confirmedHtlcTxs.flatMap(htlcTx => claimHtlcTxOutputs(params, commitmentKeys, revocationKey, htlcTx, feerates, finalScriptPubKey))
+ val penaltyTxs = confirmedHtlcTxs.flatMap(htlcTx => claimHtlcTxOutputs(commitmentKeys, revocationKey, toSelfDelay, commitmentFormat, htlcTx, dustLimit, feerates, finalScriptPubKey))
ThirdStageTransactions(penaltyTxs.toSeq)
}
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 8c3b574..fd70b66 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
@@ -396,9 +396,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Some(c: Closing.RevokedClose) =>
Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.channelParams, channelKeys, closing.commitments.remotePerCommitmentSecrets, c.revokedCommitPublished.commitTx).foreach {
case (commitmentNumber, remotePerCommitmentSecret) =>
- val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, c.revokedCommitPublished.commitTx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey)
+ // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect.
+ val toSelfDelay = closing.commitments.latest.remoteCommitParams.toSelfDelay
+ val commitmentFormat = closing.commitments.latest.commitmentFormat
+ val dustLimit = closing.commitments.latest.localCommitParams.dustLimit
+ val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, c.revokedCommitPublished.commitTx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey)
doPublish(c.revokedCommitPublished, secondStageTransactions)
- val thirdStageTransactions = Closing.RevokedClose.claimHtlcTxsOutputs(closing.commitments.channelParams, channelKeys, remotePerCommitmentSecret, c.revokedCommitPublished, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey)
+ val thirdStageTransactions = Closing.RevokedClose.claimHtlcTxsOutputs(closing.commitments.channelParams, channelKeys, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, c.revokedCommitPublished, dustLimit, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey)
doPublish(c.revokedCommitPublished, thirdStageTransactions)
}
case None =>
@@ -423,7 +427,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
closing.revokedCommitPublished.foreach(rvk => {
Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.channelParams, channelKeys, closing.commitments.remotePerCommitmentSecrets, rvk.commitTx).foreach {
case (commitmentNumber, remotePerCommitmentSecret) =>
- val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, rvk.commitTx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey)
+ // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect.
+ val toSelfDelay = closing.commitments.latest.remoteCommitParams.toSelfDelay
+ val commitmentFormat = closing.commitments.latest.commitmentFormat
+ val dustLimit = closing.commitments.latest.localCommitParams.dustLimit
+ val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, rvk.commitTx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey)
doPublish(rvk, secondStageTransactions)
}
})
@@ -620,11 +628,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String)
val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit
val nextCommitNumber = nextRemoteCommit.index
- // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our
- // counterparty, so only htlcs above remote's dust_limit matter
- val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.channelParams.remoteCommitParams.dustLimit, nextRemoteCommit.spec, commitments1.channelParams.commitmentFormat) ++
- Transactions.trimReceivedHtlcs(commitments1.channelParams.remoteCommitParams.dustLimit, nextRemoteCommit.spec, commitments1.channelParams.commitmentFormat)
- trimmedHtlcs.map(_.add).foreach { htlc =>
+ // We persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our
+ // counterparty, so only htlcs above remote's dust_limit matter.
+ val trimmedOfferedHtlcs = d.commitments.active.flatMap(c => Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet
+ val trimmedReceivedHtlcs = d.commitments.active.flatMap(c => Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet
+ (trimmedOfferedHtlcs ++ trimmedReceivedHtlcs).foreach { htlc =>
log.debug(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber")
nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry)
}
@@ -1082,8 +1090,9 @@ 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 = Funding.makeFundingScript(localFundingPubKey, msg.fundingPubKey, d.commitments.channelParams.commitmentFormat).pubkeyScript
+ val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, 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)
@@ -1103,11 +1112,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
isInitiator = false,
localContribution = spliceAck.fundingContribution,
remoteContribution = msg.fundingContribution,
- sharedInput_opt = Some(Multisig2of2Input(parentCommitment)),
+ sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = Nil,
+ commitmentFormat = commitmentFormat,
lockTime = msg.lockTime,
- dustLimit = d.commitments.channelParams.localCommitParams.dustLimit.max(d.commitments.channelParams.remoteCommitParams.dustLimit),
+ dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit),
targetFeerate = msg.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs)
)
@@ -1116,6 +1126,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = parentCommitment.localCommitParams,
+ remoteCommitParams = parentCommitment.remoteCommitParams,
channelKeys = channelKeys,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes),
localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount,
@@ -1142,20 +1154,22 @@ 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
val fundingParams = InteractiveTxParams(
channelId = d.channelId,
isInitiator = true,
localContribution = spliceInit.fundingContribution,
remoteContribution = msg.fundingContribution,
- sharedInput_opt = Some(Multisig2of2Input(parentCommitment)),
+ sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = cmd.spliceOutputs,
+ commitmentFormat = commitmentFormat,
lockTime = spliceInit.lockTime,
- dustLimit = d.commitments.channelParams.localCommitParams.dustLimit.max(d.commitments.channelParams.remoteCommitParams.dustLimit),
+ dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit),
targetFeerate = spliceInit.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs)
)
- val fundingScript = Funding.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, d.commitments.channelParams.commitmentFormat).pubkeyScript
+ val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, 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)
@@ -1167,6 +1181,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = parentCommitment.localCommitParams,
+ remoteCommitParams = parentCommitment.remoteCommitParams,
channelKeys = channelKeys,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes),
localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount,
@@ -1207,7 +1223,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, rbf.latestFundingTx.createdAt, rbf.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage)
case Right(rbf) =>
- val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
+ val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript
LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, feeCreditUsed_opt = None) match {
case Left(t) =>
log.warning("rejecting rbf request with invalid liquidity ads: {}", t.getMessage)
@@ -1223,9 +1239,10 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
isInitiator = false,
localContribution = fundingContribution,
remoteContribution = msg.fundingContribution,
- sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)),
+ sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)),
remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey,
localOutputs = rbf.latestFundingTx.fundingParams.localOutputs,
+ commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat,
lockTime = msg.lockTime,
dustLimit = rbf.latestFundingTx.fundingParams.dustLimit,
targetFeerate = msg.feerate,
@@ -1236,6 +1253,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = rbf.parentCommitment.localCommitParams,
+ remoteCommitParams = rbf.parentCommitment.remoteCommitParams,
channelKeys = channelKeys,
purpose = rbf,
localPushAmount = 0 msat, remotePushAmount = 0 msat,
@@ -1264,7 +1283,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case SpliceStatus.RbfRequested(cmd, txInitRbf) =>
getSpliceRbfContext(Some(cmd), d) match {
case Right(rbf) =>
- val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
+ val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript
LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, txInitRbf.feerate, isChannelCreation = false, msg.willFund_opt) match {
case Left(t) =>
log.info("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage)
@@ -1277,9 +1296,10 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
isInitiator = true,
localContribution = txInitRbf.fundingContribution,
remoteContribution = msg.fundingContribution,
- sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)),
+ sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)),
remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey,
localOutputs = rbf.latestFundingTx.fundingParams.localOutputs,
+ commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat,
lockTime = txInitRbf.lockTime,
dustLimit = rbf.latestFundingTx.fundingParams.dustLimit,
targetFeerate = txInitRbf.feerate,
@@ -1290,6 +1310,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = rbf.parentCommitment.localCommitParams,
+ remoteCommitParams = rbf.parentCommitment.remoteCommitParams,
channelKeys = channelKeys,
purpose = rbf,
localPushAmount = 0 msat, remotePushAmount = 0 msat,
@@ -1360,7 +1382,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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 {
- case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, d.commitments.channelParams.remoteCommitParams.htlcMinimum, purchase)
+ case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, signingSession.remoteCommitParams.htlcMinimum, purchase)
}
val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession))
stay() using d1 storing() sending commitSig
@@ -1613,11 +1635,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String)
val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit
val nextCommitNumber = nextRemoteCommit.index
- // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our
- // counterparty, so only htlcs above remote's dust_limit matter
- val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.channelParams.remoteCommitParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelParams.commitmentFormat) ++
- Transactions.trimReceivedHtlcs(d.commitments.channelParams.remoteCommitParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelParams.commitmentFormat)
- trimmedHtlcs.map(_.add).foreach { htlc =>
+ // We persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our
+ // counterparty, so only htlcs above remote's dust_limit matter.
+ val trimmedOfferedHtlcs = d.commitments.active.flatMap(c => Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet
+ val trimmedReceivedHtlcs = d.commitments.active.flatMap(c => Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet
+ (trimmedOfferedHtlcs ++ trimmedReceivedHtlcs).foreach { htlc =>
log.debug(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber")
nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry)
}
@@ -1774,7 +1796,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localChannelParams.paysClosingFees =>
// if we are not paying the closing fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation
// we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation
- val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
+ val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
if (maxFee < localClosingFees.min) {
log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min)
stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}")
@@ -1801,7 +1823,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis)
val (closingTx, closingSigned) = {
// if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee
- val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
+ val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
val nextPreferredFee = MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee)
MutualClose.makeClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee))
}
@@ -2148,14 +2170,18 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
revokedCommitPublished = d.revokedCommitPublished.map(rvk => {
// If the tx is one of our peer's HTLC txs, they were able to claim the output before us.
// In that case, we immediately publish a penalty transaction spending their HTLC tx to steal their funds.
- val (rvk1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.channelParams, channelKeys, d.commitments.remotePerCommitmentSecrets, rvk, tx, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey)
+ // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect.
+ val toSelfDelay = d.commitments.latest.remoteCommitParams.toSelfDelay
+ val commitmentFormat = d.commitments.latest.commitmentFormat
+ val dustLimit = d.commitments.latest.localCommitParams.dustLimit
+ val (rvk1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.channelParams, channelKeys, d.commitments.remotePerCommitmentSecrets, toSelfDelay, commitmentFormat, rvk, tx, dustLimit, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey)
doPublish(rvk1, penaltyTxs)
Closing.updateIrrevocablySpent(rvk1, tx)
})
)
// if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund
if (d1.localCommitPublished.exists(_.commitTx.txid == tx.txid)) {
- context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.channelParams.localCommitParams.toSelfDelay.toInt))
+ context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.latest.localCommitParams.toSelfDelay.toInt))
}
// if the local or remote commitment tx just got confirmed, we abandon anchor transactions that were created based
// on the other commitment: they will never confirm so we must free their wallet inputs.
@@ -2239,7 +2265,7 @@ 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.channelParams.commitmentFormat match {
+ d.commitments.latest.commitmentFormat match {
case commitmentFormat: Transactions.AnchorOutputsCommitmentFormat =>
val commitment = d.commitments.latest
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
@@ -2443,7 +2469,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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 commitSig = d.signingSession.remoteCommit.sign(d.channelParams, channelKeys, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput)
+ 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)
}
@@ -2456,7 +2483,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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 commitSig = signingSession.remoteCommit.sign(d.commitments.channelParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
+ 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
@@ -2467,7 +2495,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// 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, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
+ 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
@@ -2495,7 +2523,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
+ 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)
@@ -2556,7 +2584,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// 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 commitSig = signingSession.remoteCommit.sign(d.commitments.channelParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
+ 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
@@ -2567,7 +2596,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// 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, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
+ 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)
@@ -2656,7 +2685,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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.channelParams.commitmentFormat, d.commitments.latest.capacity)
+ 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)
}
@@ -2862,7 +2891,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// slightly before us. In that case, the WatchConfirmed may trigger first, and it would be inefficient to let the
// WatchPublished override our funding status: it will make us set a new WatchConfirmed that will instantly
// trigger and rewrite the funding status again.
- val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case f: LocalFundingStatus.ConfirmedFundingTx => f.tx }.exists(_.txid == w.tx.txid)
+ val alreadyConfirmed = d.commitments.active.exists(c => c.fundingTxId == w.tx.txid && c.localFundingStatus.isInstanceOf[LocalFundingStatus.ConfirmedFundingTx])
if (alreadyConfirmed) {
stay()
} else {
@@ -3122,11 +3151,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
private def handleCurrentFeerate(c: CurrentFeerates, d: ChannelDataWithCommitments) = {
val commitments = d.commitments.latest
- val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.channelParams.commitmentFormat, commitments.capacity)
+ val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, commitments.capacity)
val currentFeeratePerKw = commitments.localCommit.spec.commitTxFeerate
val shouldUpdateFee = d.commitments.localChannelParams.paysCommitTxFees && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
val shouldClose = !d.commitments.localChannelParams.paysCommitTxFees &&
- nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.channelParams.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) &&
+ nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.latest.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) &&
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
if (shouldUpdateFee) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
@@ -3147,11 +3176,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
*/
private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: ChannelDataWithCommitments) = {
val commitments = d.commitments.latest
- val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.channelParams.commitmentFormat, commitments.capacity)
+ val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, commitments.capacity)
val currentFeeratePerKw = commitments.localCommit.spec.commitTxFeerate
// if the network fees are too high we risk to not be able to confirm our current commitment
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
- nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.channelParams.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) &&
+ nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.latest.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) &&
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
if (shouldClose) {
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {
@@ -3340,12 +3369,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)
val fundingContribution = InteractiveTxFunder.computeSpliceContribution(
isInitiator = true,
- sharedInput = Multisig2of2Input(parentCommitment),
+ sharedInput = SharedFundingInput(channelKeys, parentCommitment),
spliceInAmount = cmd.additionalLocalFunding,
spliceOut = cmd.spliceOutputs,
targetFeerate = targetFeerate)
val commitTxFees = if (d.commitments.localChannelParams.paysCommitTxFees) {
- Transactions.commitTxTotalCost(d.commitments.channelParams.remoteCommitParams.dustLimit, parentCommitment.remoteCommit.spec, d.commitments.channelParams.commitmentFormat)
+ Transactions.commitTxTotalCost(parentCommitment.remoteCommitParams.dustLimit, parentCommitment.remoteCommit.spec, parentCommitment.commitmentFormat)
} else {
0.sat
}
@@ -3375,7 +3404,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// We use the same contribution as the previous splice attempt.
val fundingContribution = rbf.latestFundingTx.fundingParams.localContribution
val commitTxFees = if (d.commitments.localChannelParams.paysCommitTxFees) {
- Transactions.commitTxTotalCost(d.commitments.channelParams.remoteCommitParams.dustLimit, rbf.parentCommitment.remoteCommit.spec, d.commitments.channelParams.commitmentFormat)
+ Transactions.commitTxTotalCost(rbf.parentCommitment.remoteCommitParams.dustLimit, rbf.parentCommitment.remoteCommit.spec, rbf.latestFundingTx.fundingParams.commitmentFormat)
} else {
0.sat
}
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 e89ae08..dbc079c 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
@@ -19,7 +19,6 @@ package fr.acinq.eclair.channel.fsm
import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter}
import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
-import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel._
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs}
@@ -27,6 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.io.Peer.{LiquidityPurchaseSigned, OpenChannelResponse}
+import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{ToMilliSatoshiConversion, randomBytes32}
@@ -140,19 +140,14 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions {
case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) =>
val localFundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey
- val fundingScript = Funding.makeFundingScript(localFundingPubkey, open.fundingPubkey, d.init.channelType.commitmentFormat).pubkeyScript
+ val fundingScript = Transactions.makeFundingScript(localFundingPubkey, open.fundingPubkey, d.init.channelType.commitmentFormat).pubkeyScript
Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, fundingScript, remoteNodeId, d.init.localChannelParams.initFeatures, d.init.remoteInit.features, d.init.fundingContribution_opt) match {
case Left(t) => handleLocalError(t, d, Some(open))
case Right((channelFeatures, remoteShutdownScript, willFund_opt)) =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isOpener = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate)))
val remoteChannelParams = RemoteChannelParams(
nodeId = remoteNodeId,
- dustLimit = open.dustLimit,
- maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat,
initialRequestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity
- htlcMinimum = open.htlcMinimum,
- toRemoteDelay = open.toSelfDelay,
- maxAcceptedHtlcs = open.maxAcceptedHtlcs,
revocationBasepoint = open.revocationBasepoint,
paymentBasepoint = open.paymentBasepoint,
delayedPaymentBasepoint = open.delayedPaymentBasepoint,
@@ -162,6 +157,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
// We've exchanged open_channel2 and accept_channel2, we now know the final channelId.
val channelId = Helpers.computeChannelId(open.revocationBasepoint, channelKeys.revocationBasePoint)
val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, d.init.localChannelParams, remoteChannelParams, open.channelFlags)
+ val localCommitParams = CommitParams(d.init.proposedCommitParams.localDustLimit, d.init.proposedCommitParams.localHtlcMinimum, d.init.proposedCommitParams.localMaxHtlcValueInFlight, d.init.proposedCommitParams.localMaxAcceptedHtlcs, open.toSelfDelay)
+ val remoteCommitParams = CommitParams(open.dustLimit, open.htlcMinimum, open.maxHtlcValueInFlightMsat, open.maxAcceptedHtlcs, d.init.proposedCommitParams.toRemoteDelay)
val localAmount = d.init.fundingContribution_opt.map(_.fundingAmount).getOrElse(0 sat)
val tlvs: Set[AcceptDualFundedChannelTlv] = Set(
d.init.localChannelParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)),
@@ -174,12 +171,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
val accept = AcceptDualFundedChannel(
temporaryChannelId = open.temporaryChannelId,
fundingAmount = localAmount,
- dustLimit = d.init.proposedCommitParams.localDustLimit,
- maxHtlcValueInFlightMsat = d.init.proposedCommitParams.localMaxHtlcValueInFlight,
- htlcMinimum = d.init.proposedCommitParams.localHtlcMinimum,
+ dustLimit = localCommitParams.dustLimit,
+ maxHtlcValueInFlightMsat = localCommitParams.maxHtlcValueInFlight,
+ htlcMinimum = localCommitParams.htlcMinimum,
minimumDepth = channelParams.minDepth(nodeParams.channelConf.minDepth).getOrElse(0).toLong,
- toSelfDelay = d.init.proposedCommitParams.toRemoteDelay,
- maxAcceptedHtlcs = d.init.proposedCommitParams.localMaxAcceptedHtlcs,
+ toSelfDelay = remoteCommitParams.toSelfDelay,
+ maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs,
fundingPubkey = localFundingPubkey,
revocationBasepoint = channelKeys.revocationBasePoint,
paymentBasepoint = d.init.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint),
@@ -200,6 +197,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
sharedInput_opt = None,
remoteFundingPubKey = open.fundingPubkey,
localOutputs = Nil,
+ commitmentFormat = d.init.channelType.commitmentFormat,
lockTime = open.lockTime,
dustLimit = open.dustLimit.max(accept.dustLimit),
targetFeerate = open.fundingFeerate,
@@ -209,12 +207,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
- channelParams, channelKeys, purpose,
+ channelParams, localCommitParams, remoteCommitParams, channelKeys, purpose,
localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount,
willFund_opt.map(_.purchase),
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
- goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept
+ goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, localCommitParams, remoteCommitParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept
}
case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
@@ -238,12 +236,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId))
val remoteChannelParams = RemoteChannelParams(
nodeId = remoteNodeId,
- dustLimit = accept.dustLimit,
- maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
initialRequestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity
- htlcMinimum = accept.htlcMinimum,
- toRemoteDelay = accept.toSelfDelay,
- maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
revocationBasepoint = accept.revocationBasepoint,
paymentBasepoint = accept.paymentBasepoint,
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
@@ -252,6 +245,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
upfrontShutdownScript_opt = remoteShutdownScript)
// We start the interactive-tx funding protocol.
val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, d.init.localChannelParams, remoteChannelParams, d.lastSent.channelFlags)
+ val localCommitParams = CommitParams(d.init.proposedCommitParams.localDustLimit, d.init.proposedCommitParams.localHtlcMinimum, d.init.proposedCommitParams.localMaxHtlcValueInFlight, d.init.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay)
+ val remoteCommitParams = CommitParams(accept.dustLimit, accept.htlcMinimum, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.init.proposedCommitParams.toRemoteDelay)
val localAmount = d.lastSent.fundingAmount
val remoteAmount = accept.fundingAmount
val fundingParams = InteractiveTxParams(
@@ -262,6 +257,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
sharedInput_opt = None,
remoteFundingPubKey = accept.fundingPubkey,
localOutputs = Nil,
+ commitmentFormat = d.init.channelType.commitmentFormat,
lockTime = d.lastSent.lockTime,
dustLimit = d.lastSent.dustLimit.max(accept.dustLimit),
targetFeerate = d.lastSent.fundingFeerate,
@@ -271,12 +267,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
- channelParams, channelKeys, purpose,
+ channelParams, localCommitParams, remoteCommitParams, channelKeys, purpose,
localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount,
liquidityPurchase_opt = liquidityPurchase_opt,
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
- goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo))
+ goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, localCommitParams, remoteCommitParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo))
}
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) =>
@@ -330,9 +326,9 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
d.deferred.foreach(self ! _)
d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi))
liquidityPurchase_opt.collect {
- case purchase if !status.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, status.fundingTx.txId, status.fundingTxIndex, d.channelParams.remoteCommitParams.htlcMinimum, purchase)
+ case purchase if !status.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, status.fundingTx.txId, status.fundingTxIndex, d.remoteCommitParams.htlcMinimum, purchase)
}
- val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status, None)
+ val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status)
goto(WAIT_FOR_DUAL_FUNDING_SIGNED) using d1 storing() sending commitSig
case f: InteractiveTxBuilder.Failed =>
d.replyTo_opt.foreach(_ ! OpenChannelResponse.Rejected(f.cause.getMessage))
@@ -542,7 +538,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks)
stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, d.latestFundingTx.createdAt, d.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage)
} else {
- val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
+ val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript
LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, None) match {
case Left(t) =>
log.warning("rejecting rbf attempt: invalid liquidity ads request ({})", t.getMessage)
@@ -564,6 +560,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = d.commitments.active.head.localCommitParams,
+ remoteCommitParams = d.commitments.active.head.remoteCommitParams,
channelKeys = channelKeys,
purpose = InteractiveTxBuilder.FundingTxRbf(d.commitments.active.head, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
@@ -600,7 +598,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
lockTime = cmd.lockTime,
targetFeerate = cmd.targetFeerate,
)
- val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
+ val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript
LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, cmd.targetFeerate, isChannelCreation = true, msg.willFund_opt) match {
case Left(t) =>
log.warning("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage)
@@ -612,6 +610,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.channelParams,
+ localCommitParams = d.commitments.active.head.localCommitParams,
+ remoteCommitParams = d.commitments.active.head.remoteCommitParams,
channelKeys = channelKeys,
purpose = InteractiveTxBuilder.FundingTxRbf(d.commitments.active.head, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
@@ -695,7 +695,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
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 {
- case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, d.commitments.channelParams.remoteCommitParams.htlcMinimum, purchase)
+ case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, signingSession.remoteCommitParams.htlcMinimum, purchase)
}
val d1 = d.copy(status = DualFundingStatus.RbfWaitingForSigs(signingSession))
stay() using d1 storing() sending commitSig
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 4332b50..23d0b5b 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
@@ -110,12 +110,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isOpener = false, open.temporaryChannelId, open.feeratePerKw, None))
val remoteChannelParams = RemoteChannelParams(
nodeId = remoteNodeId,
- dustLimit = open.dustLimitSatoshis,
- maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat,
initialRequestedChannelReserve_opt = Some(open.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance
- htlcMinimum = open.htlcMinimumMsat,
- toRemoteDelay = open.toSelfDelay,
- maxAcceptedHtlcs = open.maxAcceptedHtlcs,
revocationBasepoint = open.revocationBasepoint,
paymentBasepoint = open.paymentBasepoint,
delayedPaymentBasepoint = open.delayedPaymentBasepoint,
@@ -124,17 +119,19 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
upfrontShutdownScript_opt = remoteShutdownScript)
val fundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey
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 accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
- dustLimitSatoshis = d.initFundee.proposedCommitParams.localDustLimit,
- maxHtlcValueInFlightMsat = d.initFundee.proposedCommitParams.localMaxHtlcValueInFlight,
+ dustLimitSatoshis = localCommitParams.dustLimit,
+ maxHtlcValueInFlightMsat = localCommitParams.maxHtlcValueInFlight,
channelReserveSatoshis = d.initFundee.localChannelParams.initialRequestedChannelReserve_opt.get,
minimumDepth = channelParams.minDepth(nodeParams.channelConf.minDepth).getOrElse(0).toLong,
- htlcMinimumMsat = d.initFundee.proposedCommitParams.localHtlcMinimum,
- toSelfDelay = d.initFundee.proposedCommitParams.toRemoteDelay,
- maxAcceptedHtlcs = d.initFundee.proposedCommitParams.localMaxAcceptedHtlcs,
+ htlcMinimumMsat = localCommitParams.htlcMinimum,
+ toSelfDelay = remoteCommitParams.toSelfDelay,
+ maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs,
fundingPubkey = fundingPubkey,
revocationBasepoint = channelKeys.revocationBasePoint,
paymentBasepoint = d.initFundee.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint),
@@ -145,7 +142,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)
))
- goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept
+ 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
}
case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
@@ -156,32 +153,29 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
})
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
- case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open)) =>
- Helpers.validateParamsSingleFundedFunder(nodeParams, init.channelType, init.localChannelParams.initFeatures, init.remoteInit.features, open, accept) match {
+ case Event(accept: AcceptChannel, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
+ Helpers.validateParamsSingleFundedFunder(nodeParams, d.initFunder.channelType, d.initFunder.localChannelParams.initFeatures, d.initFunder.remoteInit.features, d.lastSent, accept) match {
case Left(t) =>
d.initFunder.replyTo ! OpenChannelResponse.Rejected(t.getMessage)
handleLocalError(t, d, Some(accept))
case Right((channelFeatures, remoteShutdownScript)) =>
val remoteChannelParams = RemoteChannelParams(
nodeId = remoteNodeId,
- dustLimit = accept.dustLimitSatoshis,
- maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
initialRequestedChannelReserve_opt = Some(accept.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance
- htlcMinimum = accept.htlcMinimumMsat,
- toRemoteDelay = accept.toSelfDelay,
- maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
revocationBasepoint = accept.revocationBasepoint,
paymentBasepoint = accept.paymentBasepoint,
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
htlcBasepoint = accept.htlcBasepoint,
- initFeatures = init.remoteInit.features,
+ initFeatures = d.initFunder.remoteInit.features,
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)))
- wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).pipeTo(self)
- val channelParams = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localChannelParams, remoteChannelParams, open.channelFlags)
- goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo)
+ 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)
+ 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)
}
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
@@ -206,16 +200,17 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
val temporaryChannelId = d.channelParams.channelId
// let's create the first commitment tx that spends the yet uncommitted funding tx
val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
- val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0)
- val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint)
- Funding.makeFirstCommitTxs(d.channelParams, localFundingAmount = d.fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = d.pushAmount, remotePushAmount = 0 msat, d.commitTxFeerate, fundingTx.txid, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match {
+ val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0, d.commitmentFormat)
+ val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint, d.commitmentFormat)
+ 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!")
- val localSigOfRemoteTx = d.channelParams.commitmentFormat match {
+ val localSigOfRemoteTx = d.commitmentFormat match {
case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig
case _: SimpleTaprootChannelCommitmentFormat => ???
}
+ 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,
@@ -229,7 +224,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
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.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint), fundingCreated, d.replyTo) sending fundingCreated
+ 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) =>
@@ -258,17 +253,17 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
case Event(FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), 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)
- val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint)
+ 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, localFundingAmount = 0 sat, remoteFundingAmount = d.fundingAmount, localPushAmount = 0 msat, remotePushAmount = d.pushAmount, d.commitTxFeerate, fundingTxId, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match {
+ 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 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)
case true =>
- val localSigOfRemoteTx = d.channelParams.commitmentFormat match {
+ val localSigOfRemoteTx = d.commitmentFormat match {
case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig
case _: SimpleTaprootChannelCommitmentFormat => ???
}
@@ -280,10 +275,15 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
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,
- localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil),
+ 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(
@@ -325,10 +325,15 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
val commitment = Commitment(
fundingTxIndex = 0,
firstRemoteCommitIndex = 0,
+ fundingInput = d.localCommitTx.input.outPoint,
+ fundingAmount = d.localCommitTx.input.txOut.amount,
remoteFundingPubKey = d.remoteFundingPubKey,
localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(d.fundingTx)),
remoteFundingStatus = RemoteFundingStatus.NotLocked,
- localCommit = LocalCommit(0, d.localSpec, d.localCommitTx.tx.txid, d.localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil),
+ commitmentFormat = d.commitmentFormat,
+ localCommitParams = d.localCommitParams,
+ localCommit = LocalCommit(0, d.localSpec, d.localCommitTx.tx.txid, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil),
+ remoteCommitParams = d.remoteCommitParams,
remoteCommit = d.remoteCommit,
nextRemoteCommit_opt = None
)
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 007bf00..531e5b7 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
@@ -44,7 +44,7 @@ trait CommonFundingHandlers extends CommonHandlers {
*/
def watchFundingSpent(commitment: Commitment, additionalKnownSpendingTxs: Set[TxId], delay_opt: Option[FiniteDuration]): Unit = {
val knownSpendingTxs = commitment.commitTxIds.txIds ++ additionalKnownSpendingTxs
- val watch = WatchFundingSpent(self, commitment.commitInput.outPoint.txid, commitment.commitInput.outPoint.index.toInt, knownSpendingTxs)
+ val watch = WatchFundingSpent(self, commitment.fundingInput.txid, commitment.fundingInput.index.toInt, knownSpendingTxs)
delay_opt match {
case Some(delay) => context.system.scheduler.scheduleOnce(delay, blockchain.toClassic, watch)
case None => blockchain ! watch
@@ -84,8 +84,8 @@ trait CommonFundingHandlers extends CommonHandlers {
context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx))
d.commitments.all.find(_.fundingTxId == w.tx.txid) match {
case Some(c) =>
- val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.commitInput.outPoint.index.toInt)
- val fundingStatus = ConfirmedFundingTx(w.tx, scid, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
+ val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.fundingInput.index.toInt)
+ val fundingStatus = ConfirmedFundingTx(w.tx.txOut(c.fundingInput.index.toInt), scid, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
// When a splice transaction confirms, it double-spends all the commitment transactions that only applied to the
// previous funding transaction. Our peer cannot publish the corresponding revoked commitments anymore, so we can
// clean-up the htlc data that we were storing for the matching penalty transactions.
@@ -149,7 +149,7 @@ trait CommonFundingHandlers extends CommonHandlers {
remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)
)
peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0)
- DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice)
+ DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, SpliceStatus.NoSplice, None, None, None)
}
def delayEarlyAnnouncementSigs(remoteAnnSigs: AnnouncementSignatures): Unit = {
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 cafade4..261e1ca 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
@@ -224,7 +224,7 @@ trait ErrorHandlers extends CommonHandlers {
/** Publish 2nd-stage transactions for our local commitment. */
def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = {
- val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.commitInput.outPoint, "commit-tx", Closing.commitTxFee(commitment.commitInput, lcp.commitTx, commitment.localChannelParams.paysCommitTxFees), None)
+ val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees), None)
val publishAnchorTx_opt = txs.anchorTx_opt match {
case Some(anchorTx) if !lcp.isConfirmed =>
val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf)
@@ -265,7 +265,7 @@ trait ErrorHandlers extends CommonHandlers {
log.warning(s"they published their current commit in txid=${commitTx.txid}")
require(commitTx.txid == commitments.remoteCommit.txId, "txid mismatch")
val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d)
- context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, d.commitments.localChannelParams.paysCommitTxFees), "remote-commit"))
+ context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "remote-commit"))
val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
@@ -284,7 +284,7 @@ trait ErrorHandlers extends CommonHandlers {
require(commitTx.txid == remoteCommit.txId, "txid mismatch")
val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d)
- context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput, commitTx, d.commitments.localChannelParams.paysCommitTxFees), "next-remote-commit"))
+ context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "next-remote-commit"))
val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
@@ -332,9 +332,13 @@ trait ErrorHandlers extends CommonHandlers {
val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d)
Closing.RevokedClose.getRemotePerCommitmentSecret(d.commitments.channelParams, channelKeys, d.commitments.remotePerCommitmentSecrets, tx) match {
case Some((commitmentNumber, remotePerCommitmentSecret)) =>
- val (revokedCommitPublished, closingTxs) = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.channelParams, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
+ // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect.
+ val toSelfDelay = commitment.remoteCommitParams.toSelfDelay
+ val commitmentFormat = commitment.commitmentFormat
+ val dustLimit = commitment.localCommitParams.dustLimit
+ val (revokedCommitPublished, closingTxs) = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.channelParams, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
log.warning("txid={} was a revoked commitment, publishing the penalty tx", tx.txid)
- context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput, tx, d.commitments.localChannelParams.paysCommitTxFees), "revoked-commit"))
+ context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "revoked-commit"))
val exc = FundingTxSpent(d.channelId, tx.txid)
val error = Error(d.channelId, exc.getMessage)
val nextData = d match {
@@ -348,10 +352,10 @@ trait ErrorHandlers extends CommonHandlers {
case None => d match {
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT =>
log.warning("they published a future commit (because we asked them to) in txid={}", tx.txid)
- context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput, tx, d.commitments.latest.localChannelParams.paysCommitTxFees), "future-remote-commit"))
+ context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "future-remote-commit"))
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint
val commitKeys = d.commitments.latest.remoteKeys(channelKeys, remotePerCommitmentPoint)
- val mainTx_opt = Closing.RemoteClose.claimMainOutput(d.commitments.channelParams, commitKeys, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
+ val mainTx_opt = Closing.RemoteClose.claimMainOutput(commitKeys, tx, d.commitments.latest.localCommitParams.dustLimit, d.commitments.latest.commitmentFormat, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
mainTx_opt.foreach(tx => log.warning("publishing our recovery transaction: tx={}", tx.toString))
val remoteCommitPublished = RemoteCommitPublished(
commitTx = tx,
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 a52d189..ec33147 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
@@ -34,7 +34,7 @@ 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.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
-import fr.acinq.eclair.transactions.Transactions.{InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64}
@@ -101,15 +101,8 @@ object InteractiveTxBuilder {
case class RequireConfirmedInputs(forLocal: Boolean, forRemote: Boolean)
/** An input that is already shared between participants (e.g. the current funding output when doing a splice). */
- sealed trait SharedFundingInput {
- // @formatter:off
- def info: InputInfo
- def weight: Int
- // @formatter:on
- }
-
- case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput {
- override val weight: Int = 388
+ 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 = {
val localFundingKey = channelKeys.fundingKey(fundingTxIndex)
@@ -117,11 +110,12 @@ object InteractiveTxBuilder {
}
}
- object Multisig2of2Input {
- def apply(commitment: Commitment): Multisig2of2Input = Multisig2of2Input(
- info = commitment.commitInput,
+ object SharedFundingInput {
+ def apply(channelKeys: ChannelKeys, commitment: Commitment): SharedFundingInput = SharedFundingInput(
+ info = commitment.commitInput(channelKeys),
fundingTxIndex = commitment.fundingTxIndex,
- remoteFundingPubkey = commitment.remoteFundingPubKey
+ remoteFundingPubkey = commitment.remoteFundingPubKey,
+ commitmentFormat = commitment.commitmentFormat,
)
}
@@ -145,6 +139,7 @@ object InteractiveTxBuilder {
sharedInput_opt: Option[SharedFundingInput],
remoteFundingPubKey: PublicKey,
localOutputs: List[TxOut],
+ commitmentFormat: CommitmentFormat,
lockTime: Long,
dustLimit: Satoshi,
targetFeerate: FeeratePerKw,
@@ -388,6 +383,8 @@ object InteractiveTxBuilder {
nodeParams: NodeParams,
fundingParams: InteractiveTxParams,
channelParams: ChannelParams,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
channelKeys: ChannelKeys,
purpose: Purpose,
localPushAmount: MilliSatoshi,
@@ -425,7 +422,7 @@ object InteractiveTxBuilder {
replyTo ! LocalFailure(InvalidLiquidityAdsPaymentType(channelParams.channelId, liquidityPurchase_opt.get.paymentDetails.paymentType, Set(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc)))
Behaviors.stopped
} else {
- val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, channelKeys, fundingParams, purpose, localPushAmount, remotePushAmount, liquidityPurchase_opt, wallet, stash, context)
+ val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, localCommitParams, remoteCommitParams, channelKeys, fundingParams, purpose, localPushAmount, remotePushAmount, liquidityPurchase_opt, wallet, stash, context)
actor.start()
}
case Abort => Behaviors.stopped
@@ -444,6 +441,8 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
sessionId: ByteVector32,
nodeParams: NodeParams,
channelParams: ChannelParams,
+ localCommitParams: CommitParams,
+ remoteCommitParams: CommitParams,
channelKeys: ChannelKeys,
fundingParams: InteractiveTxBuilder.InteractiveTxParams,
purpose: Purpose,
@@ -739,7 +738,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
// operation, remote post-splice reserve may actually be worse than before, but that's not their fault.
} else {
// If remote removes funds from the channel, it must meet reserve requirements post-splice
- val remoteReserve = channelParams.remoteChannelReserveForCapacity(fundingParams.fundingAmount, isSplice = true)
+ val remoteReserve = (fundingParams.fundingAmount / 100).max(fundingParams.dustLimit)
if (sharedOutput.remoteAmount < remoteReserve) {
log.warn("invalid interactive tx: peer takes too much funds out and falls below the channel reserve ({} < {})", sharedOutput.remoteAmount, remoteReserve)
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId))
@@ -833,14 +832,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
val fundingTx = completeTx.buildUnsignedTx()
val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript)
val liquidityFee = fundingParams.liquidityFees(liquidityPurchase_opt)
- val localCommitmentKeys = LocalCommitmentKeys(channelParams, channelKeys, purpose.localCommitIndex)
- val remoteCommitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, purpose.remotePerCommitmentPoint)
- Funding.makeCommitTxs(channelParams,
+ val localCommitmentKeys = LocalCommitmentKeys(channelParams, channelKeys, purpose.localCommitIndex, fundingParams.commitmentFormat)
+ val remoteCommitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, purpose.remotePerCommitmentPoint, fundingParams.commitmentFormat)
+ Funding.makeCommitTxs(channelParams, localCommitParams, remoteCommitParams,
fundingAmount = fundingParams.fundingAmount,
toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFee,
toRemote = completeTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFee,
localHtlcs = purpose.localHtlcs,
purpose.commitTxFeerate,
+ fundingParams.commitmentFormat,
fundingTxIndex = purpose.fundingTxIndex,
fundingTx.txid, fundingOutputIndex,
localFundingKey, fundingParams.remoteFundingPubKey,
@@ -851,16 +851,16 @@ 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!")
- channelParams.commitmentFormat match {
+ fundingParams.commitmentFormat match {
case _: SegwitV0CommitmentFormat =>
val localSigOfRemoteTx = remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig
val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures)
- val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid, localCommitTx.input)
+ 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 => ???
-
+ case _: SimpleTaprootChannelCommitmentFormat =>
+ ???
}
}
}
@@ -897,7 +897,9 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
fundingParams,
purpose.fundingTxIndex,
signedTx,
+ localCommitParams,
Left(localCommit),
+ remoteCommitParams,
remoteCommit,
liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator))
)
@@ -920,9 +922,10 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
val tx = unsignedTx.buildUnsignedTx()
- val sharedSig_opt = fundingParams.sharedInput_opt.collect {
- case i: Multisig2of2Input => i.sign(channelKeys, tx, unsignedTx.inputDetails).sig
- }
+ 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 {
@@ -1020,7 +1023,7 @@ object InteractiveTxSigningSession {
// +-------+ +-------+
/** A local commitment for which we haven't received our peer's signatures. */
- case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo)
+ case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId)
private def shouldSignFirst(isInitiator: Boolean, channelParams: ChannelParams, tx: SharedTransaction): Boolean = {
val sharedAmountIn = tx.sharedInput_opt.map(_.txOut.amount).getOrElse(0 sat)
@@ -1046,18 +1049,19 @@ object InteractiveTxSigningSession {
log.info("invalid tx_signatures: witness count mismatch (expected={}, got={})", partiallySignedTx.tx.remoteInputs.length, remoteSigs.witnesses.length)
return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
}
- val sharedSigs_opt = fundingParams.sharedInput_opt match {
- case Some(sharedInput: Multisig2of2Input) =>
- (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match {
+ 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
- Some(Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey))
+ 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 None => None
- }
+ case _: SimpleTaprootChannelCommitmentFormat => ???
+ }
+ })
val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt)
if (remoteSigs.txId != txWithSigs.signedTx.txid) {
log.info("invalid tx_signatures: txId mismatch (expected={}, got={})", txWithSigs.signedTx.txid, remoteSigs.txId)
@@ -1092,10 +1096,11 @@ object InteractiveTxSigningSession {
case class WaitingForSigs(fundingParams: InteractiveTxParams,
fundingTxIndex: Long,
fundingTx: PartiallySignedSharedTransaction,
+ localCommitParams: CommitParams,
localCommit: Either[UnsignedLocalCommit, LocalCommit],
+ remoteCommitParams: CommitParams,
remoteCommit: RemoteCommit,
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
- val commitInput: InputInfo = localCommit.fold(_.input, _.input)
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 {
@@ -1103,15 +1108,26 @@ object InteractiveTxSigningSession {
case Right(commit) => commit.index + 1
}
+ def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex)
+
+ 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))
+ InputInfo(fundingOutput, TxOut(fundingParams.fundingAmount, fundingScript))
+ }
+
+ def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys))
+
def receiveCommitSig(channelParams: ChannelParams, channelKeys: ChannelKeys, remoteCommitSig: CommitSig, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = {
localCommit match {
case Left(unsignedLocalCommit) =>
- val fundingKey = channelKeys.fundingKey(fundingTxIndex)
- val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex)
- LocalCommit.fromCommitSig(channelParams, commitKeys, fundingTx.txId, fundingKey, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec).map { signedLocalCommit =>
+ val fundingKey = localFundingKey(channelKeys)
+ val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex, fundingParams.commitmentFormat)
+ val fundingOutput = commitInput(fundingKey)
+ LocalCommit.fromCommitSig(channelParams, localCommitParams, commitKeys, fundingTx.txId, fundingKey, fundingParams.remoteFundingPubKey, fundingOutput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, fundingParams.commitmentFormat).map { signedLocalCommit =>
if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) {
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, currentBlockHeight, fundingParams, liquidityPurchase_opt)
- val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None)
+ val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingOutput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, fundingParams.commitmentFormat, localCommitParams, signedLocalCommit, remoteCommitParams, remoteCommit, None)
SendingSigs(fundingStatus, commitment, fundingTx.localSigs)
} else {
this.copy(localCommit = Right(signedLocalCommit))
@@ -1135,8 +1151,9 @@ object InteractiveTxSigningSession {
Left(f)
case Right(fullySignedTx) =>
log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length)
+ val fundingOutput = commitInput(channelKeys)
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, currentBlockHeight, fundingParams, liquidityPurchase_opt)
- val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None)
+ val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingOutput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, fundingParams.commitmentFormat, localCommitParams, signedLocalCommit, remoteCommitParams, remoteCommit, None)
Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs))
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala
index 564a425..540650c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala
@@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto.keymanager
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.Features
import fr.acinq.eclair.channel.ChannelParams
-import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
/**
* Created by t-bast on 10/04/2025.
@@ -67,14 +67,14 @@ case class LocalCommitmentKeys(ourDelayedPaymentKey: PrivateKey,
}
object LocalCommitmentKeys {
- def apply(params: ChannelParams, channelKeys: ChannelKeys, localCommitIndex: Long): LocalCommitmentKeys = {
+ def apply(params: ChannelParams, channelKeys: ChannelKeys, localCommitIndex: Long, commitmentFormat: CommitmentFormat): LocalCommitmentKeys = {
val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex)
LocalCommitmentKeys(
ourDelayedPaymentKey = channelKeys.delayedPaymentKey(localPerCommitmentPoint),
- theirPaymentPublicKey = params.commitmentFormat match {
- case DefaultCommitmentFormat if params.channelFeatures.hasFeature(Features.StaticRemoteKey) => params.remoteParams.paymentBasepoint
+ theirPaymentPublicKey = commitmentFormat match {
+ case DefaultCommitmentFormat if params.localParams.walletStaticPaymentBasepoint.nonEmpty => params.remoteParams.paymentBasepoint
case DefaultCommitmentFormat => ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.paymentBasepoint, localPerCommitmentPoint)
- case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => params.remoteParams.paymentBasepoint
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => params.remoteParams.paymentBasepoint
},
ourPaymentBasePoint = params.localParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint),
ourHtlcKey = channelKeys.htlcKey(localPerCommitmentPoint),
@@ -116,11 +116,11 @@ case class RemoteCommitmentKeys(ourPaymentKey: Either[PublicKey, PrivateKey],
}
object RemoteCommitmentKeys {
- def apply(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = {
+ def apply(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey, commitmentFormat: CommitmentFormat): RemoteCommitmentKeys = {
RemoteCommitmentKeys(
ourPaymentKey = params.localParams.walletStaticPaymentBasepoint match {
case Some(walletPublicKey) => Left(walletPublicKey)
- case None => params.commitmentFormat match {
+ case None => commitmentFormat match {
// Note that if we're using option_static_remotekey, a walletStaticPaymentBasepoint will be provided.
case DefaultCommitmentFormat => Right(channelKeys.paymentKey(remotePerCommitmentPoint))
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Right(channelKeys.paymentBaseSecret)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
index d670ce6..7a481db 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala
@@ -77,16 +77,11 @@ object OpenChannelInterceptor {
}
}
- def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], walletStaticPaymentBasepoint_opt: Option[PublicKey], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): LocalChannelParams = {
+ def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], walletStaticPaymentBasepoint_opt: Option[PublicKey], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi): LocalChannelParams = {
LocalChannelParams(
nodeParams.nodeId,
nodeParams.channelKeyManager.newFundingKeyPath(isChannelOpener),
- dustLimit = nodeParams.channelConf.dustLimit,
- maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlight(fundingAmount, unlimitedMaxHtlcValueInFlight),
- initialRequestedChannelReserve_opt = if (dualFunded) None else Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit
- htlcMinimum = nodeParams.channelConf.htlcMinimum,
- toRemoteDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay
- maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs,
+ initialRequestedChannelReserve_opt = if (dualFunded) None else Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit,
isChannelOpener = isChannelOpener,
paysCommitTxFees = paysCommitTxFees,
upfrontShutdownScript_opt = upfrontShutdownScript_opt,
@@ -128,7 +123,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
val upfrontShutdownScript = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.UpfrontShutdownScript)
// If we're purchasing liquidity, we expect our peer to contribute at least the amount we're purchasing, otherwise we'll cancel the funding attempt.
val expectedFundingAmount = request.open.fundingAmount + request.open.requestFunding_opt.map(_.requestedAmount).getOrElse(0 sat)
- val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount, request.open.disableMaxHtlcValueInFlight)
+ val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount)
peer ! Peer.SpawnChannelInitiator(request.replyTo, request.open, ChannelConfig.standard, channelType, localParams)
waitForRequest()
}
@@ -161,8 +156,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
isChannelOpener = false,
paysCommitTxFees = nonInitiatorPaysCommitTxFees,
dualFunded = dualFunded,
- fundingAmount = request.fundingAmount,
- disableMaxHtlcValueInFlight = false
+ fundingAmount = request.fundingAmount
)
checkRateLimits(request, channelType, localParams)
case Left(ex) =>
@@ -196,10 +190,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
request.open.fold(_ => None, _.requestFunding_opt) match {
case Some(requestFunding) if Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.OnTheFlyFunding) && localParams.paysCommitTxFees =>
val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt)
- // Now that we know how much we'll contribute to the funding transaction, we update the maxHtlcValueInFlight.
- val maxHtlcValueInFlight = Seq(localParams.maxHtlcValueInFlightMsat, nodeParams.channelConf.maxHtlcValueInFlight(request.fundingAmount + addFunding.fundingAmount, unlimited = false)).max
- val localParams1 = localParams.copy(maxHtlcValueInFlightMsat = maxHtlcValueInFlight)
- val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams1, request.peerConnection.toClassic)
+ val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams, request.peerConnection.toClassic)
checkNoExistingChannel(request, accept)
case _ =>
// We don't honor liquidity ads for new channels: node operators should use plugin for that.
@@ -302,7 +293,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
}
}
- private def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript: Boolean, channelType: SupportedChannelType, isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, disableMaxHtlcValueInFlight: Boolean): LocalChannelParams = {
+ private def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript: Boolean, channelType: SupportedChannelType, isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi): LocalChannelParams = {
makeChannelParams(
nodeParams, initFeatures,
// Note that if our bitcoin node is configured to use taproot, this will generate a taproot script.
@@ -314,8 +305,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
isChannelOpener = isChannelOpener,
paysCommitTxFees = paysCommitTxFees,
dualFunded = dualFunded,
- fundingAmount,
- disableMaxHtlcValueInFlight
+ fundingAmount
)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
index 30e343a..36eaa37 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
@@ -602,8 +602,8 @@ object OriginSerializer extends MinimalSerializer({
})
// @formatter:off
-case class CommitmentJson(fundingTxIndex: Long, fundingTx: InputInfo, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit])
-object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.commitInput, c.localFundingStatus, c.remoteFundingStatus, c.localCommit, c.remoteCommit, c.nextRemoteCommit_opt.map(_.commit)))
+case class CommitmentJson(fundingTxIndex: Long, fundingInput: OutPoint, fundingAmount: Satoshi, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, commitmentFormat: String, localCommitParams: CommitParams, localCommit: LocalCommit, remoteCommitParams: CommitParams, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit])
+object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.fundingInput, c.fundingAmount, c.localFundingStatus, c.remoteFundingStatus, c.commitmentFormat.toString, c.localCommitParams, c.localCommit, c.remoteCommitParams, c.remoteCommit, c.nextRemoteCommit_opt.map(_.commit)))
// @formatter:on
// @formatter:off
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
index 14b8226..08549f8 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
@@ -301,7 +301,7 @@ object OnTheFlyFunding {
private def relay(data: DATA_NORMAL): Behavior[Command] = {
context.log.debug("relaying {} on-the-fly HTLCs that have been funded", cmd.proposed.size)
- val htlcMinimum = data.commitments.channelParams.remoteCommitParams.htlcMinimum
+ val htlcMinimum = data.commitments.latest.remoteCommitParams.htlcMinimum
val cmdAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedCommandResponse)
val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED[Origin.Hot, HtlcResult]](WrappedHtlcSettled)
cmd.proposed.foldLeft(cmd.status.remainingFees) {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala
index adf9ffb..43b8816 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala
@@ -17,7 +17,7 @@
package fr.acinq.eclair.reputation
import fr.acinq.bitcoin.scalacompat.ByteVector32
-import fr.acinq.eclair.channel.{ChannelJammingException, ChannelParams, Commitments, IncomingConfidenceTooLow, OutgoingConfidenceTooLow, TooManySmallHtlcs}
+import fr.acinq.eclair.channel._
import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, TimestampMilli}
@@ -120,8 +120,8 @@ case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, Pend
}
object Reputation {
- val endorsementLevels = 8
- val maxEndorsement = endorsementLevels - 1
+ private val endorsementLevels = 8
+ val maxEndorsement: Int = endorsementLevels - 1
case class Config(enabled: Boolean, halfLife: FiniteDuration, maxRelayDuration: FiniteDuration)
@@ -131,29 +131,29 @@ object Reputation {
* @param incomingConfidence Confidence that the outgoing HTLC will succeed given the reputation of the incoming peer
*/
case class Score(incomingConfidence: Double, outgoingConfidence: Double) {
- val endorsement = toEndorsement(incomingConfidence)
+ val endorsement: Int = toEndorsement(incomingConfidence)
- def checkOutgoingChannelOccupancy(outgoingHtlcs: Seq[UpdateAddHtlc], params: ChannelParams): Either[ChannelJammingException, Unit] = {
- val maxAcceptedHtlcs = Seq(params.localCommitParams.maxAcceptedHtlcs, params.remoteCommitParams.maxAcceptedHtlcs).min
+ def checkOutgoingChannelOccupancy(channelId: ByteVector32, commitment: Commitment, outgoingHtlcs: Seq[UpdateAddHtlc]): Either[ChannelJammingException, Unit] = {
+ val maxAcceptedHtlcs = Seq(commitment.localCommitParams.maxAcceptedHtlcs, commitment.remoteCommitParams.maxAcceptedHtlcs).min
for ((amountMsat, i) <- outgoingHtlcs.map(_.amountMsat).sorted.zipWithIndex) {
// We want to allow some small HTLCs but still keep slots for larger ones.
// We never want to reject HTLCs of size above `maxHtlcAmount / maxAcceptedHtlcs` as too small because we want to allow filling all the slots with HTLCs of equal sizes.
// We use exponentially spaced thresholds in between.
- if (amountMsat.toLong < 1 || amountMsat.toLong.toDouble < math.pow(params.maxHtlcValueInFlight.toLong.toDouble / maxAcceptedHtlcs, i / maxAcceptedHtlcs)) {
- return Left(TooManySmallHtlcs(params.channelId, number = i + 1, below = amountMsat))
+ if (amountMsat.toLong < 1 || amountMsat.toLong.toDouble < math.pow(commitment.maxHtlcValueInFlight.toLong.toDouble / maxAcceptedHtlcs, i / maxAcceptedHtlcs)) {
+ return Left(TooManySmallHtlcs(channelId, number = i + 1, below = amountMsat))
}
}
val htlcValueInFlight = outgoingHtlcs.map(_.amountMsat).sum
val slotsOccupancy = outgoingHtlcs.size.toDouble / maxAcceptedHtlcs
- val valueOccupancy = htlcValueInFlight.toLong.toDouble / params.maxHtlcValueInFlight.toLong.toDouble
+ val valueOccupancy = htlcValueInFlight.toLong.toDouble / commitment.maxHtlcValueInFlight.toLong.toDouble
val occupancy = slotsOccupancy max valueOccupancy
// Because there are only 8 endorsement levels, the highest endorsement corresponds to a confidence between 87.5% and 100%.
// So even for well-behaved peers setting the highest endorsement we still expect a confidence of less than 93.75%.
// To compensate for that we add a tolerance of 10% that's also useful for nodes without history.
if (incomingConfidence + 0.1 < occupancy) {
- return Left(IncomingConfidenceTooLow(params.channelId, incomingConfidence, occupancy))
+ return Left(IncomingConfidenceTooLow(channelId, incomingConfidence, occupancy))
}
Right(())
@@ -168,7 +168,7 @@ object Reputation {
}
case object Score {
- val max = Score(1.0, 1.0)
+ val max: Score = Score(1.0, 1.0)
def fromEndorsement(endorsement: Int): Score = Score((endorsement + 0.5) / 8, 1.0)
}
@@ -178,9 +178,9 @@ object Reputation {
def incomingOccupancy(commitments: Commitments): Double = {
commitments.active.map(commitment => {
val incomingHtlcs = commitment.localCommit.spec.htlcs.collect(DirectedHtlc.incoming)
- val slotsOccupancy = incomingHtlcs.size.toDouble / (commitments.channelParams.localCommitParams.maxAcceptedHtlcs min commitments.channelParams.remoteCommitParams.maxAcceptedHtlcs)
+ val slotsOccupancy = commitments.active.map(c => incomingHtlcs.size.toDouble / (c.localCommitParams.maxAcceptedHtlcs min c.remoteCommitParams.maxAcceptedHtlcs)).max
val htlcValueInFlight = incomingHtlcs.toSeq.map(_.amountMsat).sum
- val valueOccupancy = htlcValueInFlight.toLong.toDouble / commitments.channelParams.maxHtlcValueInFlight.toLong.toDouble
+ val valueOccupancy = commitments.active.map(c => htlcValueInFlight.toLong.toDouble / c.maxHtlcValueInFlight.toLong.toDouble).max
slotsOccupancy max valueOccupancy
}).max
}
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 cb68430..e9e7f5b 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
@@ -58,6 +58,8 @@ object Transactions {
sealed trait CommitmentFormat {
// @formatter:off
+ /** Weight of a fully signed channel output, when spent by a [[ChannelSpendTransaction]]. */
+ def fundingInputWeight: Int
/** Weight of a fully signed [[CommitTx]] transaction without any HTLCs. */
def commitWeight: Int
/** Weight of a fully signed [[ClaimLocalAnchorTx]] or [[ClaimRemoteAnchorTx]] input. */
@@ -93,7 +95,9 @@ object Transactions {
// @formatter:on
}
- sealed trait SegwitV0CommitmentFormat extends CommitmentFormat
+ sealed trait SegwitV0CommitmentFormat extends CommitmentFormat {
+ override val fundingInputWeight = 384
+ }
/**
* Commitment format as defined in the v1.0 specification (https://github.com/lightningnetwork/lightning-rfc/tree/v1.0).
@@ -115,6 +119,8 @@ object Transactions {
override val htlcOfferedPenaltyWeight = 572
override val htlcReceivedPenaltyWeight = 577
override val claimHtlcPenaltyWeight = 484
+
+ override def toString: String = "legacy"
}
/**
@@ -149,19 +155,24 @@ object Transactions {
* Don't use this commitment format unless you know what you're doing!
* See https://lists.linuxfoundation.org/pipermail/lightning-dev/2020-September/002796.html for details.
*/
- case object UnsafeLegacyAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat
+ case object UnsafeLegacyAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat {
+ override def toString: String = "unsafe_anchor_outputs"
+ }
/**
* This commitment format removes the fees from the pre-signed 2nd-stage htlc transactions to fix the fee inflating
* attack against [[UnsafeLegacyAnchorOutputsCommitmentFormat]].
*/
- case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat
+ case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat {
+ override def toString: String = "anchor_outputs"
+ }
sealed trait TaprootCommitmentFormat extends CommitmentFormat
sealed trait SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat {
// weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and
// not in variable length DER format (around 72 bytes)
+ override val fundingInputWeight = 230
override val commitWeight = 960
override val anchorInputWeight = 230
override val htlcOutputWeight = 172
@@ -180,13 +191,15 @@ object Transactions {
override val claimHtlcPenaltyWeight = 396
}
- case object LegacySimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat
+ case object LegacySimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat {
+ override def toString: String = "unsafe_simple_taproot"
+ }
- case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat
+ case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat {
+ override def toString: String = "simple_taproot"
+ }
- // TODO: we're currently keeping the now unused redeemScript to avoid a painful codec update. When creating v5 codecs
- // (for taproot channels), don't forget to remove this field from the InputInfo class!
- case class InputInfo(outPoint: OutPoint, txOut: TxOut, unusedRedeemScript: ByteVector)
+ case class InputInfo(outPoint: OutPoint, txOut: TxOut)
// @formatter:off
/** This trait contains redeem information necessary to spend different types of segwit inputs. */
@@ -598,7 +611,7 @@ object Transactions {
outputIndex: Int,
commitmentFormat: CommitmentFormat): UnsignedHtlcSuccessTx = {
val htlc = output.htlc.add
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val tx = Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
@@ -652,7 +665,7 @@ object Transactions {
outputIndex: Int,
commitmentFormat: CommitmentFormat): UnsignedHtlcTimeoutTx = {
val htlc = output.htlc.add
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val tx = Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
@@ -699,7 +712,7 @@ object Transactions {
findPubKeyScriptIndex(htlcTx, pubkeyScript) match {
case Left(skip) => Left(skip)
case Right(outputIndex) =>
- val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.htlcDelayedWeight)
val tx = Transaction(
version = 2,
@@ -762,7 +775,7 @@ object Transactions {
def findInput(commitTx: Transaction, outputs: Seq[CommitmentOutput], htlc: UpdateAddHtlc): Option[InputInfo] = {
outputs.zipWithIndex.collectFirst {
case (OutHtlc(outgoingHtlc, _, _), outputIndex) if outgoingHtlc.add.id == htlc.id =>
- InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
}
}
@@ -822,7 +835,7 @@ object Transactions {
def findInput(commitTx: Transaction, outputs: Seq[CommitmentOutput], htlc: UpdateAddHtlc): Option[InputInfo] = {
outputs.zipWithIndex.collectFirst {
case (InHtlc(incomingHtlc, _, _), outputIndex) if incomingHtlc.add.id == htlc.id =>
- InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
}
}
@@ -903,7 +916,7 @@ object Transactions {
def findInput(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = {
val pubKeyScript = redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat).pubkeyScript
- findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty))
+ findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)))
}
def createUnsignedTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimLocalAnchorTx] = {
@@ -940,7 +953,7 @@ object Transactions {
def findInput(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = {
val pubKeyScript = redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat).pubkeyScript
- findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty))
+ findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)))
}
def createUnsignedTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteAnchorTx] = {
@@ -973,7 +986,7 @@ object Transactions {
commitKeys.ourPaymentKey match {
case Left(_) => Left(OutputAlreadyInWallet)
case Right(_) =>
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toRemoteWeight)
val tx = Transaction(
version = 2,
@@ -1026,7 +1039,7 @@ object Transactions {
commitKeys.ourPaymentKey match {
case Left(_) => Left(OutputAlreadyInWallet)
case Right(_) =>
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toRemoteWeight)
val tx = Transaction(
version = 2,
@@ -1075,7 +1088,7 @@ object Transactions {
findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match {
case Left(skip) => Left(skip)
case Right(outputIndex) =>
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toLocalDelayedWeight)
val tx = Transaction(
version = 2,
@@ -1123,7 +1136,7 @@ object Transactions {
findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match {
case Left(skip) => Left(skip)
case Right(outputIndex) =>
- val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.mainPenaltyWeight)
val tx = Transaction(
version = 2,
@@ -1200,7 +1213,7 @@ object Transactions {
localFinalScriptPubKey: ByteVector,
feerate: FeeratePerKw,
commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcPenaltyTx] = {
- val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex))
val amount = input.txOut.amount - weight2fee(feerate, redeemDetails.weight)
val tx = Transaction(
version = 2,
@@ -1252,7 +1265,7 @@ object Transactions {
// Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx.
htlcTx.txOut.zipWithIndex.collect {
case (txOut, outputIndex) if txOut.publicKeyScript == redeemInfo.pubkeyScript =>
- val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), ByteVector.empty)
+ val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcPenaltyWeight)
val tx = Transaction(
version = 2,
@@ -1502,6 +1515,19 @@ object Transactions {
}
}
+ def makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo = {
+ commitmentFormat match {
+ case _: SegwitV0CommitmentFormat => RedeemInfo.P2wsh(Script.write(multiSig2of2(localFundingKey, remoteFundingKey)))
+ case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(Taproot.musig2Aggregate(localFundingKey, remoteFundingKey), None)
+ }
+ }
+
+ def makeFundingInputInfo(fundingTxId: TxId, fundingOutputIndex: Int, fundingAmount: Satoshi, localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = {
+ val redeemInfo = makeFundingScript(localFundingKey, remoteFundingKey, commitmentFormat)
+ val fundingTxOut = TxOut(fundingAmount, redeemInfo.pubkeyScript)
+ InputInfo(OutPoint(fundingTxId, fundingOutputIndex), fundingTxOut)
+ }
+
// @formatter:off
/** We always create multiple versions of each closing transaction, where fees are either paid by us or by our peer. */
sealed trait SimpleClosingTxFee
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala
index 779490f..b8f7b20 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala
@@ -22,6 +22,7 @@ import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1
import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2
import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3
import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4
+import fr.acinq.eclair.wire.internal.channel.version5.ChannelCodecs5
import grizzled.slf4j.Logging
import scodec.Codec
import scodec.codecs.{byte, discriminated}
@@ -67,7 +68,8 @@ object ChannelCodecs extends Logging {
* More info here: https://github.com/scodec/scodec/issues/122
*/
val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(byte)
- .typecase(4, ChannelCodecs4.channelDataCodec)
+ .typecase(5, ChannelCodecs5.channelDataCodec)
+ .typecase(4, ChannelCodecs4.channelDataCodec.decodeOnly)
.typecase(3, ChannelCodecs3.channelDataCodec.decodeOnly)
.typecase(2, ChannelCodecs2.channelDataCodec.decodeOnly)
.typecase(1, ChannelCodecs1.channelDataCodec.decodeOnly)
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 8c2a81a..1ecc72f 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
@@ -57,7 +57,7 @@ private[channel] object ChannelCodecs0 {
// field and don't support additional features which is why all bits are set to 0.
)
- def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalChannelParams] = (
+ def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[ChannelTypes0.LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimit" | satoshi) ::
@@ -71,7 +71,7 @@ private[channel] object ChannelCodecs0 {
("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) ::
("features" | combinedFeaturesCodec)).map {
case nodeId :: channelPath :: dustLimit :: maxHtlcValueInFlightMsat :: channelReserve :: htlcMinimum :: toSelfDelay :: maxAcceptedHtlcs :: isInitiator :: upfrontShutdownScript_opt :: walletStaticPaymentBasepoint :: features :: HNil =>
- LocalChannelParams(nodeId, channelPath, dustLimit, maxHtlcValueInFlightMsat, channelReserve, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isInitiator, isInitiator, upfrontShutdownScript_opt, walletStaticPaymentBasepoint, features)
+ ChannelTypes0.LocalParams(nodeId, channelPath, dustLimit, maxHtlcValueInFlightMsat, channelReserve, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isInitiator, isInitiator, upfrontShutdownScript_opt, walletStaticPaymentBasepoint, features)
}.decodeOnly
val remoteParamsCodec: Codec[ChannelTypes0.RemoteParams] = (
@@ -129,7 +129,7 @@ private[channel] object ChannelCodecs0 {
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | varsizebinarydata)).map {
- case outpoint :: txOut :: _ :: HNil => InputInfo(outpoint, txOut, ByteVector.empty)
+ case outpoint :: txOut :: _ :: HNil => InputInfo(outpoint, txOut)
}.decodeOnly
private val missingHtlcExpiry: Codec[CltvExpiry] = provide(CltvExpiry(0))
@@ -420,7 +420,7 @@ private[channel] object ChannelCodecs0 {
("closeStatus" | provide(Option.empty[CloseStatus]))).map {
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil =>
val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)
- DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice)
+ DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, SpliceStatus.NoSplice, localShutdown, remoteShutdown, closingFeerates)
}.decodeOnly
val DATA_NORMAL_10_Codec: Codec[DATA_NORMAL] = (
@@ -434,7 +434,7 @@ private[channel] object ChannelCodecs0 {
("closeStatus" | provide(Option.empty[CloseStatus]))).map {
case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil =>
val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)
- DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice)
+ DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, SpliceStatus.NoSplice, localShutdown, remoteShutdown, closingFeerates)
}.decodeOnly
val DATA_SHUTDOWN_04_Codec: Codec[DATA_SHUTDOWN] = (
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala
index 1121805..568cafb 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala
@@ -17,7 +17,7 @@
package fr.acinq.eclair.wire.internal.channel.version0
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
-import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxId, TxOut}
+import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxId, TxOut}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.CommitmentSpec
@@ -95,7 +95,7 @@ private[channel] object ChannelTypes0 {
* the raw transaction. It provides more information for auditing but is not used for business logic, so we can safely
* put dummy values in the migration.
*/
- def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), ByteVector.empty), tx, None)
+ def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil)), tx, None)
case class HtlcTxAndSigs(txinfo: UnsignedHtlcTx, localSig: ByteVector64, remoteSig: ByteVector64)
@@ -104,11 +104,11 @@ private[channel] object ChannelTypes0 {
// Before version3, we stored fully signed local transactions (commit tx and htlc txs). It meant that someone gaining
// access to the database could publish revoked commit txs, so we changed that to only store remote signatures.
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs) {
- def migrate(remoteFundingPubKey: PublicKey): channel.LocalCommit = {
+ def migrate(remoteFundingPubKey: PublicKey): (channel.LocalCommit, InputInfo) = {
val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey)
val unsignedCommitTx = publishableTxs.commitTx.copy(tx = removeWitnesses(publishableTxs.commitTx.tx))
val htlcRemoteSigs = publishableTxs.htlcTxsAndSigs.map(_.remoteSig)
- channel.LocalCommit(index, spec, unsignedCommitTx.tx.txid, unsignedCommitTx.input, remoteSig, htlcRemoteSigs)
+ (channel.LocalCommit(index, spec, unsignedCommitTx.tx.txid, remoteSig, htlcRemoteSigs), unsignedCommitTx.input)
}
private def extractRemoteSig(commitTx: CommitTx, remoteFundingPubKey: PublicKey): ChannelSpendSignature.IndividualSignature = {
@@ -161,6 +161,31 @@ private[channel] object ChannelTypes0 {
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
+ case class LocalParams(nodeId: PublicKey,
+ fundingKeyPath: DeterministicWallet.KeyPath,
+ dustLimit: Satoshi,
+ maxHtlcValueInFlightMsat: UInt64,
+ initialRequestedChannelReserve_opt: Option[Satoshi],
+ htlcMinimum: MilliSatoshi,
+ toSelfDelay: CltvExpiryDelta,
+ maxAcceptedHtlcs: Int,
+ isChannelOpener: Boolean,
+ paysCommitTxFees: Boolean,
+ upfrontShutdownScript_opt: Option[ByteVector],
+ walletStaticPaymentBasepoint: Option[PublicKey],
+ initFeatures: Features[InitFeature]) {
+ def migrate(): channel.LocalChannelParams = channel.LocalChannelParams(
+ nodeId = nodeId,
+ fundingKeyPath = fundingKeyPath,
+ initialRequestedChannelReserve_opt = initialRequestedChannelReserve_opt,
+ isChannelOpener = isChannelOpener,
+ paysCommitTxFees = paysCommitTxFees,
+ upfrontShutdownScript_opt = upfrontShutdownScript_opt,
+ walletStaticPaymentBasepoint = walletStaticPaymentBasepoint,
+ initFeatures = initFeatures,
+ )
+ }
+
case class RemoteParams(nodeId: PublicKey,
dustLimit: Satoshi,
maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi
@@ -177,12 +202,7 @@ private[channel] object ChannelTypes0 {
upfrontShutdownScript_opt: Option[ByteVector]) {
def migrate(): channel.RemoteChannelParams = channel.RemoteChannelParams(
nodeId = nodeId,
- dustLimit = dustLimit,
- maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat,
initialRequestedChannelReserve_opt = requestedChannelReserve_opt,
- htlcMinimum = htlcMinimum,
- toRemoteDelay = toRemoteDelay,
- maxAcceptedHtlcs = maxAcceptedHtlcs,
revocationBasepoint = revocationBasepoint,
paymentBasepoint = paymentBasepoint,
delayedPaymentBasepoint = delayedPaymentBasepoint,
@@ -195,7 +215,7 @@ private[channel] object ChannelTypes0 {
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long)
case class Commitments(channelVersion: ChannelVersion,
- localParams: LocalChannelParams, remoteParams: RemoteParams,
+ localParams: LocalParams, remoteParams: RemoteParams,
channelFlags: ChannelFlags,
localCommit: LocalCommit, remoteCommit: RemoteCommit,
localChanges: LocalChanges, remoteChanges: RemoteChanges,
@@ -210,25 +230,34 @@ private[channel] object ChannelTypes0 {
} else {
ChannelConfig()
}
- val channelFeatures = if (channelVersion.hasAnchorOutputs) {
- ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
- } else if (channelVersion.hasStaticRemotekey) {
- ChannelFeatures(Features.StaticRemoteKey)
+ val commitmentFormat = if (channelVersion.hasAnchorOutputs) {
+ UnsafeLegacyAnchorOutputsCommitmentFormat
} else {
- ChannelFeatures()
+ DefaultCommitmentFormat
}
+ val (localCommit1, commitInput) = localCommit.migrate(remoteParams.fundingPubKey)
+ val localCommitParams = CommitParams(localParams.dustLimit, localParams.htlcMinimum, localParams.maxHtlcValueInFlightMsat, localParams.maxAcceptedHtlcs, remoteParams.toRemoteDelay)
+ val remoteCommitParams = CommitParams(remoteParams.dustLimit, remoteParams.htlcMinimum, remoteParams.maxHtlcValueInFlightMsat, remoteParams.maxAcceptedHtlcs, localParams.toSelfDelay)
val commitment = Commitment(
fundingTxIndex = 0,
firstRemoteCommitIndex = 0,
+ fundingInput = commitInput.outPoint,
+ fundingAmount = commitInput.txOut.amount,
remoteFundingPubKey = remoteParams.fundingWhy this scored 35/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.