Add support for zero-fee commitment format (#3192)
What changed, and why it matters
This commit adds an experimental new Lightning channel type to Eclair called 'zero-fee commitments.' It is a large feature patch implementing a draft BOLT specification (PR 1228), not a fix for a known vulnerability. The change introduces new transaction formats, validation rules, and force-close behavior. Because it is experimental, disabled by default, and extensively tested, it is unlikely to be an immediate security issue, but any large change to consensus-sensitive transaction code carries risk of subtle bugs.
Treat this as a high-risk feature addition requiring careful review and testing before enabling in production. Keep the feature disabled by default until the BOLT specification is finalized and the implementation has been audited. Monitor for follow-up fixes related to transaction validation, fee accounting, and edge cases in force-close paths.
Security signals we found
Large feature addition (+1618/-276 across 42 files) touching consensus-sensitive transaction construction and signing
New experimental channel type disabled by default; requires explicit opt-in via eclair.conf
Implements draft BOLT specification (lightning/bolts#1228), not a finalized standard
Changes force-close behavior: no longer auto-publishes local commit tx alone for zero-fee channels; bundles with anchor tx
Changes error handling: ignores remote 'error' message for zero-fee channels instead of force-closing
Adds new validation limits (max 114 HTLCs, 0 sat/byte feerate, no update_fee)
Renames and restructures claim transactions for remote commitment outputs
Adds new P2A (pay-to-anchor) redeem path and v3 transaction version handling
Evidence from the diff
The commit implements zero-fee commitment channels using Bitcoin v3/TRUC transactions and a shared pay-to-anchor (P2A) output. It adds a new feature bit, channel type, commitment format, transaction construction logic, and updates force-close handling so that commit fees are paid via the anchor/main output rather than by the commit tx itself. Validation is tightened: max accepted HTLCs is capped at 114, commit feerate must be 0, and update_fee is disallowed. The change also renames ClaimRemoteDelayedOutputTx to ClaimRemoteMainOutputTx and adjusts preconditions for publishing second-stage transactions. Extensive test vectors and integration tests are included.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scalaeclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scalaeclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scalaeclair-core/src/main/scala/fr/acinq/eclair/Features.scalaeclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scalaeclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scalaInspect captured patch +1618 / −276
diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md
index b81810e..5fa0e55 100644
--- a/docs/release-notes/eclair-vnext.md
+++ b/docs/release-notes/eclair-vnext.md
@@ -64,6 +64,26 @@ To open a taproot channel with a node that supports the `option_simple_taproot`
$ eclair-cli open --nodeId=<node_id> --fundingSatoshis=<funding_amount> --channelType=simple_taproot_channel --announceChannel=false
```
+### Zero-fee Channels (experimental)
+
+This release adds support for zero-fee commitments (using v3/TRUC transactions), as specified in [the BOLTs](https://github.com/lightning/bolts/pull/1228).
+With this new type of channels, commitment transactions don't pay any mining fee (in most cases), which gets rid of the `update_fee` mechanism and all of the related channel reserve issues.
+It also gets rid of the undesired channel force-closes that happen when the mempool feerate spikes and channel participants disagree on what feerate to use, which has been a major source of wasted on-chain space.
+It also offers better protection against pinning attacks (thanks to package relay) and reduces the on-chain footprint compared to anchor output channels.
+
+This feature is not actived by default, because we haven't tested yet that v3/TRUC transactions always propagate to miners.
+To enable it, add the following to your `eclair.conf`:
+
+```conf
+eclair.features.zero_fee_commitments = optional
+```
+
+To open a zero-fee channel with a node that supports the `zero_fee_commitments` feature, use the following command:
+
+```sh
+$ eclair-cli open --nodeId=<node_id> --fundingSatoshis=<funding_amount> --channelType=zero_fee_commitments
+```
+
### Remove support for non-anchor channels
We remove the code used to support legacy channels that don't use anchor outputs or taproot.
diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf
index 64a988c..87741f1 100644
--- a/eclair-core/src/main/resources/reference.conf
+++ b/eclair-core/src/main/resources/reference.conf
@@ -76,6 +76,7 @@ eclair {
option_quiesce = optional
option_attribution_data = optional
option_onion_messages = optional
+ zero_fee_commitments = disabled
// This feature should only be enabled when acting as an LSP for mobile wallets.
// When activating this feature, the peer-storage section should be customized to match desired SLAs.
option_provide_storage = disabled
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 d093969..de93ef0 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
@@ -359,6 +359,11 @@ object Features {
val mandatory = 38
}
+ case object ZeroFeeCommitments extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
+ val rfcName = "zero_fee_commitments"
+ val mandatory = 40
+ }
+
case object ProvideStorage extends Feature with InitFeature with NodeFeature {
val rfcName = "option_provide_storage"
val mandatory = 42
@@ -479,6 +484,7 @@ object Features {
Quiescence,
AttributionData,
OnionMessages,
+ ZeroFeeCommitments,
ProvideStorage,
ChannelType,
ScidAlias,
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
index 9fa95e3..c1a695a 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
@@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
-import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
+import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
import fr.acinq.eclair.db._
@@ -364,7 +364,7 @@ object NodeParams extends Logging {
require(htlcMinimum > 0.msat, "channel.htlc-minimum-msat must be strictly greater than 0")
val maxAcceptedHtlcs = config.getInt("channel.max-accepted-htlcs")
- require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"channel.max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}")
+ require(maxAcceptedHtlcs <= Channel.maxAcceptedHtlcs(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), s"channel.max-accepted-htlcs must be lower than ${Channel.maxAcceptedHtlcs(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())}")
val maxToLocalCLTV = CltvExpiryDelta(config.getInt("channel.max-to-local-delay-blocks"))
val offeredCLTV = CltvExpiryDelta(config.getInt("channel.to-remote-delay-blocks"))
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
index 2a009c4..47a0458 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
@@ -93,6 +93,9 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat =>
// If the fee has a large enough change, we update the fee.
currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio
+ case Transactions.ZeroFeeCommitmentFormat =>
+ // We never send update_fee when using zero-fee commitments.
+ false
}
}
@@ -111,6 +114,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
val networkFeerate = feerates.fast
val networkMinFee = feerates.minimum
commitmentFormat match {
+ case Transactions.ZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat =>
// Since Bitcoin Core v28, 1-parent-1-child package relay has been deployed: it should be ok if the commit tx
// doesn't propagate on its own.
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 f153793..f55d606 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
@@ -104,6 +104,15 @@ object ChannelTypes {
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat
override def toString: String = s"simple_taproot_channel${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
}
+ case class ZeroFeeCommitments(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
+ override def features: Set[ChannelTypeFeature] = Set(
+ if (scidAlias) Some(Features.ScidAlias) else None,
+ if (zeroConf) Some(Features.ZeroConf) else None,
+ Some(Features.ZeroFeeCommitments)
+ ).flatten
+ override def commitmentFormat: CommitmentFormat = ZeroFeeCommitmentFormat
+ override def toString: String = s"zero_fee_commitments${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
+ }
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
override def features: Set[InitFeature] = featureBits.activated.keySet
@@ -131,6 +140,10 @@ object ChannelTypes {
SimpleTaprootChannel(zeroConf = true),
SimpleTaprootChannel(scidAlias = true),
SimpleTaprootChannel(scidAlias = true, zeroConf = true),
+ ZeroFeeCommitments(),
+ ZeroFeeCommitments(zeroConf = true),
+ ZeroFeeCommitments(scidAlias = true),
+ ZeroFeeCommitments(scidAlias = true, zeroConf = true),
SimpleTaprootChannelsPhoenix,
).map {
channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType
@@ -154,6 +167,8 @@ object ChannelTypes {
if (!announceChannel && Features.canUseFeature(localFeatures, remoteFeatures, Features.SimpleTaprootChannels)) {
// We currently only support unannounced taproot channels.
Some(SimpleTaprootChannel(scidAlias = useScidAlias))
+ } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroFeeCommitments)) {
+ Some(ZeroFeeCommitments(scidAlias = useScidAlias))
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) {
Some(AnchorOutputsZeroFeeHtlcTx(scidAlias = useScidAlias))
} else {
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 2af018d..8dcaf02 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
@@ -740,8 +740,8 @@ object Commitment {
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, commitmentFormat)
- val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs)
+ val outputs = makeCommitTxOutputs(localFundingKey.publicKey, remoteFundingPubKey, commitmentInput, commitKeys.publicKeys, channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
+ val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, commitmentFormat, outputs)
val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
(commitTx, htlcTxs)
}
@@ -755,8 +755,8 @@ object Commitment {
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, commitmentFormat)
- val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs)
+ val outputs = makeCommitTxOutputs(remoteFundingPubKey, localFundingKey.publicKey, commitmentInput, commitKeys.publicKeys, !channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
+ val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, commitmentFormat, outputs)
val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
(commitTx, htlcTxs)
}
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 2029377..4df52fb 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
@@ -114,12 +114,6 @@ object Helpers {
// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
- // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483.
- if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
-
- // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing.
- if (isFeeTooSmall(open.feeratePerKw)) return Left(FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw))
-
if (open.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit))
// BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis.
@@ -140,9 +134,15 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
channelType.commitmentFormat match {
case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
- case _: AnchorOutputsCommitmentFormat => ()
+ case _: SegwitV0CommitmentFormat => ()
}
+ // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing.
+ if (isFeeTooSmall(open.feeratePerKw, channelType)) return Left(FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw))
+
+ // BOLT #2: The receiving node MUST fail the channel if max_accepted_htlcs is too high.
+ if (open.maxAcceptedHtlcs > Channel.maxAcceptedHtlcs(channelType)) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.maxAcceptedHtlcs(channelType)))
+
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isProposedCommitFeerateTooHigh(localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
@@ -178,12 +178,6 @@ object Helpers {
// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
- // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483.
- if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
-
- // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing.
- if (isFeeTooSmall(open.commitmentFeerate)) return Left(FeerateTooSmall(open.temporaryChannelId, open.commitmentFeerate))
-
if (open.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimit, Channel.MIN_DUST_LIMIT))
if (open.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimit, nodeParams.channelConf.maxRemoteDustLimit))
@@ -193,6 +187,12 @@ 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.
+ if (isFeeTooSmall(open.commitmentFeerate, channelType)) return Left(FeerateTooSmall(open.temporaryChannelId, open.commitmentFeerate))
+
+ // BOLT #2: The receiving node MUST fail the channel if max_accepted_htlcs is too high.
+ if (open.maxAcceptedHtlcs > Channel.maxAcceptedHtlcs(channelType)) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.maxAcceptedHtlcs(channelType)))
+
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isProposedCommitFeerateTooHigh(localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
@@ -218,7 +218,7 @@ object Helpers {
case Right(channelType) => channelType
}
- if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
+ if (accept.maxAcceptedHtlcs > Channel.maxAcceptedHtlcs(channelType)) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.maxAcceptedHtlcs(channelType)))
if (accept.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit))
if (accept.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUST_LIMIT))
@@ -244,7 +244,7 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
channelType.commitmentFormat match {
case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
- case _: AnchorOutputsCommitmentFormat => ()
+ case _: SegwitV0CommitmentFormat => ()
}
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
}
@@ -268,7 +268,7 @@ object Helpers {
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (accept.pushAmount > accept.fundingAmount) return Left(InvalidPushAmount(accept.temporaryChannelId, accept.pushAmount, accept.fundingAmount.toMilliSatoshi))
- if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
+ if (accept.maxAcceptedHtlcs > Channel.maxAcceptedHtlcs(channelType)) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.maxAcceptedHtlcs(channelType)))
if (accept.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimit, Channel.MIN_DUST_LIMIT))
if (accept.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimit, nodeParams.channelConf.maxRemoteDustLimit))
@@ -291,8 +291,12 @@ object Helpers {
* @param remoteFeeratePerKw remote fee rate per kiloweight
* @return true if the remote fee rate is too small
*/
- private def isFeeTooSmall(remoteFeeratePerKw: FeeratePerKw): Boolean = {
- remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
+ private def isFeeTooSmall(remoteFeeratePerKw: FeeratePerKw, channelType: SupportedChannelType): Boolean = {
+ channelType match {
+ case _: ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
+ case _: ChannelTypes.SimpleTaprootChannel | ChannelTypes.SimpleTaprootChannelsPhoenix => remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
+ case _: ChannelTypes.ZeroFeeCommitments => false
+ }
}
/** Compute the temporaryChannelId of a dual-funded channel. */
@@ -778,7 +782,7 @@ object Helpers {
dummyClosingTxs.preferred_opt match {
case Some(dummyTx) =>
commitment.commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: SegwitV0CommitmentFormat =>
val dummyPubkey = commitment.remoteFundingPubKey
val dummySig = IndividualSignature(Transactions.PlaceHolderSig)
val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig)
@@ -816,7 +820,7 @@ object Helpers {
closingTxs.remoteOnly_opt.flatMap(tx => localSig(tx)).map(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(_)),
).flatten[ClosingCompleteTlv])
}
- case _: AnchorOutputsCommitmentFormat => TlvStream(Set(
+ case _: SegwitV0CommitmentFormat => TlvStream(Set(
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
@@ -872,7 +876,7 @@ object Helpers {
case None => Left(MissingCloseSignature(commitment.channelId))
}
}
- case _: AnchorOutputsCommitmentFormat =>
+ case _: SegwitV0CommitmentFormat =>
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
@@ -1056,7 +1060,7 @@ object Helpers {
/** Create outputs of the local commitment transaction, allowing us for example to identify HTLC outputs. */
def makeLocalCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Seq[CommitmentOutput] = {
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
- makeCommitTxOutputs(fundingKey.publicKey, commitment.remoteFundingPubKey, commitKeys.publicKeys, commitment.localChannelParams.paysCommitTxFees, commitment.localCommitParams.dustLimit, commitment.localCommitParams.toSelfDelay, commitment.localCommit.spec, commitment.commitmentFormat)
+ makeCommitTxOutputs(fundingKey.publicKey, commitment.remoteFundingPubKey, commitment.commitInput(channelKeys), commitKeys.publicKeys, commitment.localChannelParams.paysCommitTxFees, commitment.localCommitParams.dustLimit, commitment.localCommitParams.toSelfDelay, commitment.localCommit.spec, commitment.commitmentFormat)
}
/**
@@ -1189,10 +1193,10 @@ object Helpers {
object RemoteClose {
/** Transactions spending outputs of a remote commitment transaction. */
- case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteDelayedOutputTx], anchorTx_opt: Option[ClaimRemoteAnchorTx], htlcTxs: Seq[ClaimHtlcTx])
+ case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteMainOutputTx], anchorTx_opt: Option[ClaimRemoteAnchorTx], htlcTxs: Seq[ClaimHtlcTx])
/** Claim all the outputs that belong to us in the remote commitment transaction (which can be either their current or next commitment). */
- def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, closingFeerate: FeeratePerKw, finalScriptPubKey: ByteVector, spendAnchorWithoutHtlcs: Boolean)(implicit log: LoggingAdapter): (RemoteCommitPublished, SecondStageTransactions) = {
+ def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, closingFeerate: FeeratePerKw, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector, spendAnchorWithoutHtlcs: Boolean)(implicit log: LoggingAdapter): (RemoteCommitPublished, SecondStageTransactions) = {
require(remoteCommit.txId == commitTx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
@@ -1201,8 +1205,16 @@ object Helpers {
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
+ // When using v3 transactions, we can use our main output to pay commit fees if it's large enough.
+ // In that case, we don't need to create a dedicated anchor transaction which avoids using wallet inputs.
+ val useMainTxForAnchor = commitment.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => false
+ case ZeroFeeCommitmentFormat =>
+ val commitFee = Transactions.weight2fee(feerates.fastest, commitTx.weight())
+ mainTx_opt.exists(_.tx.txOut.map(_.amount).sum > commitFee)
+ }
val spendAnchor = incomingHtlcs.nonEmpty || outgoingHtlcs.nonEmpty || spendAnchorWithoutHtlcs
- val anchorTx_opt = if (spendAnchor) {
+ val anchorTx_opt = if (spendAnchor && !useMainTxForAnchor) {
claimAnchor(fundingKey, commitKeys, commitTx, commitment.commitmentFormat)
} else {
None
@@ -1226,10 +1238,10 @@ object Helpers {
}
/** Claim our main output from the remote commitment transaction, if available. */
- def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteDelayedOutputTx] = {
+ def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteMainOutputTx] = {
commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
- ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main") {
+ ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
}
}
}
@@ -1237,7 +1249,7 @@ object Helpers {
/** Create outputs of the remote commitment transaction, allowing us for example to identify HTLC outputs. */
def makeRemoteCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit): Seq[CommitmentOutput] = {
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
- makeCommitTxOutputs(commitment.remoteFundingPubKey, fundingKey.publicKey, commitKeys.publicKeys, !commitment.localChannelParams.paysCommitTxFees, commitment.remoteCommitParams.dustLimit, commitment.remoteCommitParams.toSelfDelay, remoteCommit.spec, commitment.commitmentFormat)
+ makeCommitTxOutputs(commitment.remoteFundingPubKey, fundingKey.publicKey, commitment.commitInput(channelKeys), commitKeys.publicKeys, !commitment.localChannelParams.paysCommitTxFees, commitment.remoteCommitParams.dustLimit, commitment.remoteCommitParams.toSelfDelay, remoteCommit.spec, commitment.commitmentFormat)
}
/**
@@ -1348,7 +1360,7 @@ object Helpers {
object RevokedClose {
/** Transactions spending outputs of a revoked remote commitment transactions. */
- case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteDelayedOutputTx], mainPenaltyTx_opt: Option[MainPenaltyTx], htlcPenaltyTxs: Seq[HtlcPenaltyTx])
+ case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteMainOutputTx], mainPenaltyTx_opt: Option[MainPenaltyTx], htlcPenaltyTxs: Seq[HtlcPenaltyTx])
/** Transactions spending outputs of confirmed remote HTLC transactions. */
case class ThirdStageTransactions(htlcDelayedPenaltyTxs: Seq[ClaimHtlcDelayedOutputPenaltyTx])
@@ -1396,8 +1408,8 @@ object Helpers {
// First we will claim our main output right away.
val mainTx_opt = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
- ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main") {
+ ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
}
}
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 af889c5..1eef0ad 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
@@ -127,11 +127,11 @@ object Channel {
}
}
- def commitParams(fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): ProposedCommitParams = ProposedCommitParams(
+ def commitParams(fundingAmount: Satoshi, channelType: SupportedChannelType, unlimitedMaxHtlcValueInFlight: Boolean): ProposedCommitParams = ProposedCommitParams(
localDustLimit = dustLimit,
localHtlcMinimum = htlcMinimum,
localMaxHtlcValueInFlight = maxHtlcValueInFlight(fundingAmount, unlimitedMaxHtlcValueInFlight),
- localMaxAcceptedHtlcs = maxAcceptedHtlcs,
+ localMaxAcceptedHtlcs = maxAcceptedHtlcs.min(Channel.maxAcceptedHtlcs(channelType)),
toRemoteDelay = toRemoteDelay,
)
}
@@ -151,7 +151,14 @@ object Channel {
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
val MAX_FUNDING_WITHOUT_WUMBO: Satoshi = 16777216 sat // = 2^24
- val MAX_ACCEPTED_HTLCS = 483
+
+ /** We limit the number of HTLCs that can be pending to ensure that the commit tx doesn't exceed standard weight limits. */
+ def maxAcceptedHtlcs(channelType: SupportedChannelType): Int = channelType match {
+ case _: ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => 483
+ case _: ChannelTypes.SimpleTaprootChannel | ChannelTypes.SimpleTaprootChannelsPhoenix => 483
+ // When using v3 transactions, the maximum package size is more restrictive than v2 transactions.
+ case _: ChannelTypes.ZeroFeeCommitments => 114
+ }
// We may need to rely on our peer's commit tx in certain cases (backup/restore) so we must ensure their transactions
// can propagate through the bitcoin network (assuming bitcoin core nodes with default policies).
@@ -399,7 +406,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val thirdStageTransactions = Closing.LocalClose.claimHtlcDelayedOutputs(c.localCommitPublished, channelKeys, commitment, closingFeerate, closing.finalScriptPubKey)
doPublish(c.localCommitPublished, thirdStageTransactions)
case Some(c: Closing.RemoteClose) =>
- val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, c.remoteCommit, c.remoteCommitPublished.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
+ val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, c.remoteCommit, c.remoteCommitPublished.commitTx, closingFeerate, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
doPublish(c.remoteCommitPublished, secondStageTransactions, commitment)
case Some(c: Closing.RecoveryClose) =>
// We cannot do anything in that case: we've already published our recovery transaction before restarting,
@@ -428,12 +435,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
doPublish(lcp, secondStageTransactions, commitment)
})
closing.remoteCommitPublished.foreach(rcp => {
- val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, commitment.remoteCommit, rcp.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
+ val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, commitment.remoteCommit, rcp.commitTx, closingFeerate, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
doPublish(rcp, secondStageTransactions, commitment)
})
closing.nextRemoteCommitPublished.foreach(rcp => {
val remoteCommit = commitment.nextRemoteCommit_opt.get
- val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, rcp.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
+ val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, rcp.commitTx, closingFeerate, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
doPublish(rcp, secondStageTransactions, commitment)
})
closing.revokedCommitPublished.foreach(rvk => {
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 ba82d37..69a4d2e 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
@@ -32,7 +32,7 @@ import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentK
import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.transactions.Transactions
-import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, TlvStream}
import fr.acinq.eclair.{MilliSatoshiLong, randomKey, toLongId}
import scodec.bits.ByteVector
@@ -79,7 +79,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
val localShutdownScript = input.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
val localNonce = input.channelType.commitmentFormat match {
case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
- case _: AnchorOutputsCommitmentFormat => None
+ case _: SegwitV0CommitmentFormat => None
}
val open = OpenChannel(
chainHash = nodeParams.chainHash,
@@ -134,7 +134,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
val localShutdownScript = d.initFundee.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
val localNonce = d.initFundee.channelType.commitmentFormat match {
case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
- case _: AnchorOutputsCommitmentFormat => None
+ case _: SegwitV0CommitmentFormat => None
}
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
dustLimitSatoshis = localCommitParams.dustLimit,
@@ -233,7 +233,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
}
case None => Left(MissingCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
}
- case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
+ case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
}
localSigOfRemoteTx match {
case Left(f) => handleLocalError(f, d, None)
@@ -303,7 +303,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
}
case None => Left(MissingCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
}
- case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
+ case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
}
localSigOfRemoteTx match {
case Left(f) => handleLocalError(f, d, Some(fc))
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 648b1fb..ca1d543 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
@@ -26,7 +26,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL}
import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.db.RevokedHtlcInfoCleaner
-import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{RealShortChannelId, ShortChannelId}
@@ -136,7 +136,7 @@ trait CommonFundingHandlers extends CommonHandlers {
val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, commitments.latest.remoteFundingPubKey, 1)
ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias, nextLocalNonce.publicNonce)
- case _: AnchorOutputsCommitmentFormat =>
+ case _: SegwitV0CommitmentFormat =>
ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias)
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
index 16df2dd..5469184 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala
@@ -24,7 +24,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.NonceGenerator
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.io.Peer
-import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol.{ClosingComplete, HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage}
import scodec.bits.ByteVector
@@ -142,7 +142,7 @@ trait CommonHandlers {
val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitments.latest.remoteFundingPubKey, commitments.latest.fundingTxId)
localCloseeNonce_opt = Some(localCloseeNonce)
Shutdown(commitments.channelId, finalScriptPubKey, localCloseeNonce.publicNonce)
- case _: AnchorOutputsCommitmentFormat =>
+ case _: SegwitV0CommitmentFormat =>
Shutdown(commitments.channelId, finalScriptPubKey)
}
}
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 ca71132..b1a053b 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
@@ -160,6 +160,12 @@ trait ErrorHandlers extends CommonHandlers {
// The channel closing is retried on every reconnect of the channel, until it succeeds.
log.warning("ignoring remote 'link failed to shutdown', probably coming from lnd")
stay() sending Warning(d.channelId, "ignoring your 'link failed to shutdown' to avoid an unnecessary force-close")
+ } else if (hasCommitments.commitments.latest.commitmentFormat == ZeroFeeCommitmentFormat) {
+ // When using v3 transactions, we want to avoid using our wallet inputs as much as possible.
+ // It is much more convenient if our peer publishes their commitment transaction if they're having an issue,
+ // because we can use our main output for pay the fees of the commitment transaction directly (if necessary).
+ log.warning("ignoring remote error: waiting for remote commit tx to be published")
+ stay() sending Warning(d.channelId, "ignoring your error: please publish your commitment if you want to force-close the channel")
} else {
spendLocalCurrent(hasCommitments, maxClosingFeerateOverride_opt = None)
}
@@ -232,8 +238,12 @@ trait ErrorHandlers extends CommonHandlers {
/** Publish 2nd-stage transactions for our local commitment. */
def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = {
- val (localFee, _) = Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees)
- val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", localFee, None)
+ val publishCommitTx_opt = commitment.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat =>
+ val (localFee, _) = Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees)
+ Some(PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", localFee, None))
+ case ZeroFeeCommitmentFormat => None // we will publish the commit-tx alongside the anchor tx
+ }
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)
@@ -242,7 +252,7 @@ trait ErrorHandlers extends CommonHandlers {
}
val publishMainDelayedTx_opt = txs.mainDelayedTx_opt.map(tx => PublishFinalTx(tx, None))
val publishHtlcTxs = txs.htlcTxs.map(htlcTx => PublishReplaceableTx(htlcTx, lcp.commitTx, commitment, Closing.confirmationTarget(htlcTx)))
- val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs
+ val publishQueue = publishCommitTx_opt.toSeq ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs
publishIfNeeded(publishQueue, lcp.irrevocablySpent)
if (!lcp.isConfirmed) {
@@ -277,7 +287,7 @@ trait ErrorHandlers extends CommonHandlers {
}
val (localFee, remoteFee) = Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees)
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, localFee, remoteFee, "remote-commit", None))
- val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
+ val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, closingFeerate, nodeParams.currentBitcoinCoreFeerates, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished))
@@ -300,7 +310,7 @@ trait ErrorHandlers extends CommonHandlers {
}
val (localFee, remoteFee) = Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees)
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, localFee, remoteFee, "next-remote-commit", None))
- val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
+ val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, closingFeerate, nodeParams.currentBitcoinCoreFeerates, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished))
@@ -317,13 +327,19 @@ trait ErrorHandlers extends CommonHandlers {
case Some(commit) if rcp.commitTx.txid == commit.txId => commit
case _ => commitment.remoteCommit
}
+ val confirmationTarget = Closing.confirmationTarget(remoteCommit, commitment.remoteCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf)
val publishAnchorTx_opt = txs.anchorTx_opt match {
- case Some(anchorTx) if !rcp.isConfirmed =>
- val confirmationTarget = Closing.confirmationTarget(remoteCommit, commitment.remoteCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf)
- Some(PublishReplaceableTx(anchorTx, rcp.commitTx, commitment, confirmationTarget))
+ case Some(anchorTx) if !rcp.isConfirmed => Some(PublishReplaceableTx(anchorTx, rcp.commitTx, commitment, confirmationTarget))
case _ => None
}
- val publishMainTx_opt = txs.mainTx_opt.map(tx => PublishFinalTx(tx, None))
+ val publishMainTx_opt = txs.mainTx_opt.map(tx => commitment.commitmentFormat match {
+ case ZeroFeeCommitmentFormat => publishAnchorTx_opt match {
+ // Instead of creating a dedicated anchor transaction, we use our main output to pay commit fees whenever possible.
+ case None if !rcp.isConfirmed => PublishReplaceableTx(tx, rcp.commitTx, commitment, confirmationTarget)
+ case _ => PublishFinalTx(tx, None)
+ }
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => PublishFinalTx(tx, None)
+ })
val publishHtlcTxs = txs.htlcTxs.map(htlcTx => PublishReplaceableTx(htlcTx, rcp.commitTx, commitment, Closing.confirmationTarget(htlcTx)))
val publishQueue = publishAnchorTx_opt ++ publishMainTx_opt ++ publishHtlcTxs
publishIfNeeded(publishQueue, rcp.irrevocablySpent)
@@ -337,7 +353,7 @@ trait ErrorHandlers extends CommonHandlers {
// we will watch for its confirmation. This ensures that we detect double-spends that could come from:
// - our own RBF attempts
// - remote transactions for outputs that both parties may spend (e.g. HTLCs)
- val watchSpentQueue = rcp.localOutput_opt ++ (if (!rcp.isConfirmed) rcp.anchorOutput_opt else None) ++ rcp.htlcOutputs.toSeq
+ val watchSpentQueue = rcp.localOutput_opt ++ (if (publishAnchorTx_opt.nonEmpty) rcp.anchorOutput_opt else None) ++ rcp.htlcOutputs.toSeq
watchSpentIfNeeded(rcp.commitTx, watchSpentQueue, rcp.irrevocablySpent)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala
index 8d7acc3..345943e 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala
@@ -126,6 +126,7 @@ private class ReplaceableTxFunder(replyTo: ActorRef[ReplaceableTxFunder.FundingR
private val dustLimit = commitment.localCommitParams.dustLimit
private val commitFee: Satoshi = commitment.capacity - commitTx.txOut.map(_.amount).sum
+ private val commitFeerate: FeeratePerKw = Transactions.fee2rate(commitFee, commitTx.weight())
private val log = context.log
@@ -133,7 +134,6 @@ private class ReplaceableTxFunder(replyTo: ActorRef[ReplaceableTxFunder.FundingR
log.info("funding {} tx (targetFeerate={})", tx.desc, targetFeerate)
tx match {
case anchorTx: ClaimAnchorTx =>
- val commitFeerate = commitment.localCommit.spec.commitTxFeerate
if (targetFeerate <= commitFeerate) {
log.info("skipping {}: commit feerate is high enough (feerate={})", tx.desc, commitFeerate)
// We set retry = true in case the on-chain feerate rises before the commit tx is confirmed: if that happens
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala
index 368cf56..e137e91 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala
@@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.publish
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior}
-import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxId}
+import fr.acinq.bitcoin.scalacompat.{Transaction, TxId}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext
@@ -42,20 +42,20 @@ object ReplaceableTxPrePublisher {
sealed trait Command
case class CheckPreconditions(replyTo: ActorRef[PreconditionsResult], txInfo: ForceCloseTransaction, commitTx: Transaction, concurrentCommitTxs: Set[TxId]) extends Command
- private case object ParentTxOk extends Command
+ private case class ParentTxOk(confirmed: Boolean) extends Command
private case object FundingTxNotFound extends Command
private case object CommitTxRecentlyConfirmed extends Command
private case object CommitTxDeeplyConfirmed extends Command
private case object ConcurrentCommitAvailable extends Command
private case object ConcurrentCommitRecentlyConfirmed extends Command
private case object ConcurrentCommitDeeplyConfirmed extends Command
- private case object HtlcOutputAlreadySpent extends Command
+ private case object OutputAlreadySpent extends Command
private case class UnknownFailure(reason: Throwable) extends Command
// @formatter:on
// @formatter:off
sealed trait PreconditionsResult
- case object PreconditionsOk extends PreconditionsResult
+ case class PreconditionsOk(commitTxConfirmed_opt: Option[Boolean]) extends PreconditionsResult
case class PreconditionsFailed(reason: TxPublisher.TxRejectedReason) extends PreconditionsResult
// @formatter:on
@@ -68,9 +68,10 @@ object ReplaceableTxPrePublisher {
txInfo match {
case _: ClaimLocalAnchorTx => prePublisher.checkLocalCommitAnchorPreconditions(commitTx)
case _: ClaimRemoteAnchorTx => prePublisher.checkRemoteCommitAnchorPreconditions(commitTx)
- case _: SignedHtlcTx | _: ClaimHtlcTx => prePublisher.checkHtlcPreconditions(txInfo.desc, txInfo.input.outPoint, commitTx, concurrentCommitTxs)
+ case _: SignedHtlcTx | _: ClaimHtlcTx => prePublisher.check2ndStageTxPreconditions(txInfo, commitTx, concurrentCommitTxs)
+ case _: ClaimRemoteMainOutputTx => prePublisher.check2ndStageTxPreconditions(txInfo, commitTx, concurrentCommitTxs)
case _ =>
- replyTo ! PreconditionsOk
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = None)
Behaviors.stopped
}
}
@@ -102,13 +103,13 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = true).flatMap {
case true =>
// The funding output is unspent: let's publish our anchor transaction to get our local commit confirmed.
- Future.successful(ParentTxOk)
+ Future.successful(ParentTxOk(confirmed = false))
case false =>
// The funding output is spent: we check whether our local commit is confirmed or in our mempool.
bitcoinClient.getTxConfirmations(commitTx.txid).transformWith {
case Success(Some(confirmations)) if confirmations >= nodeParams.channelConf.minDepth => Future.successful(CommitTxDeeplyConfirmed)
case Success(Some(confirmations)) if confirmations > 0 => Future.successful(CommitTxRecentlyConfirmed)
- case Success(Some(0)) => Future.successful(ParentTxOk) // our commit tx is unconfirmed, let's publish our anchor transaction
+ case Success(Some(0)) => Future.successful(ParentTxOk(confirmed = false)) // our commit tx is unconfirmed, let's publish our anchor transaction
case _ =>
// Our commit tx is unconfirmed and cannot be found in our mempool: this means that a remote commit is
// either confirmed or in our mempool. In that case, we don't want to use our local commit tx: the
@@ -124,8 +125,8 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
case Failure(reason) => UnknownFailure(reason)
}
Behaviors.receiveMessagePartial {
- case ParentTxOk =>
- replyTo ! PreconditionsOk
+ case ParentTxOk(confirmed) =>
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = Some(confirmed))
Behaviors.stopped
case FundingTxNotFound =>
log.debug("funding tx could not be found, we don't know yet if we need to claim our anchor")
@@ -146,7 +147,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
case UnknownFailure(reason) =>
log.error("could not check local anchor preconditions, proceeding anyway: ", reason)
// If our checks fail, we don't want it to prevent us from trying to publish our commit tx.
- replyTo ! PreconditionsOk
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = None)
Behaviors.stopped
}
}
@@ -166,7 +167,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
case true =>
// The funding output is unspent, or spent by an *unconfirmed* transaction: let's publish our anchor
// transaction, we may be able to replace our local commit with this (more interesting) remote commit.
- Future.successful(ParentTxOk)
+ Future.successful(ParentTxOk(confirmed = false))
case false =>
// The funding output is spent by a confirmed commit tx: we check the status of our anchor's commit tx.
bitcoinClient.getTxConfirmations(commitTx.txid).transformWith {
@@ -184,8 +185,8 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
case Failure(reason) => UnknownFailure(reason)
}
Behaviors.receiveMessagePartial {
- case ParentTxOk =>
- replyTo ! PreconditionsOk
+ case ParentTxOk(confirmed) =>
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = Some(confirmed))
Behaviors.stopped
case FundingTxNotFound =>
log.debug("funding tx could not be found, we don't know yet if we need to claim our anchor")
@@ -206,58 +207,67 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
case UnknownFailure(reason) =>
log.error("could not check remote anchor preconditions, proceeding anyway: ", reason)
// If our checks fail, we don't want it to prevent us from trying to publish our commit tx.
- replyTo ! PreconditionsOk
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = None)
Behaviors.stopped
}
}
/**
- * We first verify that the commit tx we're spending may confirm: if a conflicting commit tx is already confirmed, our
- * HTLC transaction has become obsolete. Then we check that the HTLC output that we're spending isn't already spent
+ * We first verify that the commit tx we're spending may confirm: if a conflicting commit tx is already confirmed,
+ * our transaction has become obsolete. Then we check that the output that we're spending isn't already spent
* by a confirmed transaction, which may happen in case of a race between HTLC-timeout and HTLC-success.
*/
- private def checkHtlcPreconditions(desc: String, input: OutPoint, commitTx: Transaction, concurrentCommitTxs: Set[TxId]): Behavior[Command] = {
+ private def check2ndStageTxPreconditions(txInfo: ForceCloseTransaction, commitTx: Transaction, concurrentCommitTxs: Set[TxId]): Behavior[Command] = {
context.pipeToSelf(bitcoinClient.getTxConfirmations(commitTx.txid).flatMap {
- case Some(_) =>
- // If the HTLC output is already spent by a confirmed transaction, there is no need for RBF: either this is one
+ case Some(confirmations) =>
+ // If the output is already spent by a confirmed transaction, there is no need for RBF: either this is one
// of our transactions (which thus has a high enough feerate), or it was a race with our peer and we lost.
- bitcoinClient.isTransactionOutputSpent(input.txid, input.index.toInt).map {
- case true => HtlcOutputAlreadySpent
- case false => ParentTxOk
+ bitcoinClient.isTransactionOutputSpent(txInfo.input.outPoint.txid, txInfo.input.outPoint.index.toInt).map {
+ case true => OutputAlreadySpent
+ case false => ParentTxOk(confirmed = confirmations > 0)
}
case None =>
- // The parent commitment is unconfirmed: we shouldn't try to publish this HTLC transaction if a concurrent
+ // The parent commitment is unconfirmed: we shouldn't try to publish this transaction if a concurrent
// commitment is deeply confirmed.
checkConcurrentCommits(concurrentCommitTxs.toSeq).map {
case Some(confirmations) if confirmations >= nodeParams.channelConf.minDepth => ConcurrentCommitDeeplyConfirmed
case Some(_) => ConcurrentCommitRecentlyConfirmed
- case None => ParentTxOk
+ case None => ParentTxOk(confirmed = false)
}
}) {
case Success(result) => result
case Failure(reason) => UnknownFailure(reason)
}
Behaviors.receiveMessagePartial {
- case ParentTxOk =>
- replyTo ! PreconditionsOk
- Behaviors.stopped
+ case ParentTxOk(confirmed) =>
+ txInfo match {
+ case _: SignedHtlcTx | _: ClaimHtlcTx if commitTx.version == 3 && !confirmed =>
+ // We don't yet use HTLC txs to spend the ephemeral anchor, so we need to wait for the commit tx to be confirmed.
+ // Otherwise, bitcoind will reject the transaction because it must spend the ephemeral anchor.
+ log.info("cannot publish v3 {} spending commitTxId={}: waiting for commit tx to confirm first", txInfo.desc, commitTx.txid)
+ replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true))
+ Behaviors.stopped
+ case _ =>
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = Some(confirmed))
+ Behaviors.stopped
+ }
case ConcurrentCommitRecentlyConfirmed =>
- log.debug("cannot publish {} spending commitTxId={}: concurrent commit tx was recently confirmed, let's check again later", desc, commitTx.txid)
+ log.debug("cannot publish {} spending commitTxId={}: concurrent commit tx was recently confirmed, let's check again later", txInfo.desc, commitTx.txid)
// We keep retrying until the concurrent commit reaches min-depth to protect against reorgs.
replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true))
Behaviors.stopped
case ConcurrentCommitDeeplyConfirmed =>
- log.warn("cannot publish {} spending commitTxId={}: concurrent commit is deeply confirmed", desc, commitTx.txid)
+ log.warn("cannot publish {} spending commitTxId={}: concurrent commit is deeply confirmed", txInfo.desc, commitTx.txid)
replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed)
Behaviors.stopped
- case HtlcOutputAlreadySpent =>
- log.warn("cannot publish {}: htlc output {} has already been spent", desc, input)
+ case OutputAlreadySpent =>
+ log.warn("cannot publish {}: output {} has already been spent", txInfo.desc, txInfo.input.outPoint)
replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed)
Behaviors.stopped
case UnknownFailure(reason) =>
- log.error(s"could not check $desc preconditions, proceeding anyway: ", reason)
- // If our checks fail, we don't want it to prevent us from trying to publish our htlc transactions.
- replyTo ! PreconditionsOk
+ log.error(s"could not check ${txInfo.desc} preconditions, proceeding anyway: ", reason)
+ // If our checks fail, we don't want it to prevent us from trying to publish our transactions.
+ replyTo ! PreconditionsOk(commitTxConfirmed_opt = None)
Behaviors.stopped
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala
index c5a294a..9c9abdd 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala
@@ -18,12 +18,13 @@ package fr.acinq.eclair.channel.publish
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
import akka.actor.typed.{ActorRef, Behavior}
-import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Transaction}
+import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.FundedTx
import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext
-import fr.acinq.eclair.transactions.Transactions.ClaimAnchorTx
+import fr.acinq.eclair.transactions.Transactions
+import fr.acinq.eclair.transactions.Transactions.{ClaimAnchorTx, ClaimRemoteMainOutputTx}
import fr.acinq.eclair.{BlockHeight, NodeParams}
import scala.concurrent.duration.{DurationInt, DurationLong}
@@ -124,7 +125,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case WrappedPreconditionsResult(result) =>
result match {
- case ReplaceableTxPrePublisher.PreconditionsOk => checkTimeLocks()
+ case ReplaceableTxPrePublisher.PreconditionsOk(commitTxConfirmed_opt) => checkTimeLocks(commitTxConfirmed_opt)
case ReplaceableTxPrePublisher.PreconditionsFailed(reason) => sendResult(TxPublisher.TxRejected(txPublishContext.id, cmd, reason), None)
}
case UpdateConfirmationTarget(target) =>
@@ -134,10 +135,11 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}
- def checkTimeLocks(): Behavior[Command] = {
+ def checkTimeLocks(commitTxConfirmed_opt: Option[Boolean]): Behavior[Command] = {
cmd.txInfo match {
// There are no time locks on anchor transactions, we can claim them right away.
case _: ClaimAnchorTx => chooseFeerate()
+ case txInfo: ClaimRemoteMainOutputTx => fundWithoutWalletInputs(txInfo, commitTxConfirmed_opt)
case _ =>
val timeLocksChecker = context.spawn(TxTimeLocksMonitor(nodeParams, bitcoinClient, txPublishContext), "time-locks-monitor")
timeLocksChecker ! TxTimeLocksMonitor.CheckTx(context.messageAdapter[TxTimeLocksMonitor.TimeLocksOk](_ => TimeLocksOk), cmd.txInfo.tx, cmd.desc)
@@ -159,7 +161,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case CheckUtxosResult(isSafe, currentBlockHeight) =>
val targetFeerate = getFeerate(nodeParams.currentBitcoinCoreFeerates, confirmationTarget, currentBlockHeight, isSafe)
- fund(targetFeerate)
+ fundWithWalletInputs(targetFeerate)
case UpdateConfirmationTarget(target) =>
confirmationTarget = target
Behaviors.same
@@ -167,7 +169,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}
- def fund(targetFeerate: FeeratePerKw): Behavior[Command] = {
+ private def fundWithWalletInputs(targetFeerate: FeeratePerKw): Behavior[Command] = {
val txFunder = context.spawn(ReplaceableTxFunder(nodeParams, bitcoinClient, txPublishContext), "tx-funder")
txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), Right(cmd.txInfo), cmd.commitTx, cmd.commitment, targetFeerate)
Behaviors.receiveMessagePartial {
@@ -199,6 +201,44 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}
+ private def fundWithoutWalletInputs(txInfo: ClaimRemoteMainOutputTx, commitTxConfirmed_opt: Option[Boolean]): Behavior[Command] = {
+ commitTxConfirmed_opt match {
+ case Some(true) =>
+ // If the commitment transaction is already confirmed, we submit our main transaction alone.
+ // Its feerate was already set by the channel actor, we only need to sign and broadcast.
+ log.info("commit-tx is already confirmed, we don't need to spend the ephemeral anchor")
+ val signedTx = txInfo.sign()
+ val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), s"mempool-tx-monitor-${signedTx.txid}")
+ txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), signedTx, parentTx_opt = None, cmd.input, nodeParams.channelConf.minDepth, cmd.desc, txInfo.fee)
+ wait(FundedTx(txInfo, walletInputs_opt = None, signedTx, Transactions.fee2rate(txInfo.fee, signedTx.weight())))
+ case _ =>
+ // Otherwise, we want our transaction to pay fees for the commitment transaction as well.
+ // Note that we don't need any wallet utxo here, we'll simply lower our output amount.
+ val targetFeerate = getFeerate(nodeParams.currentBitcoinCoreFeerates, confirmationTarget, nodeParams.currentBlockHeight, hasEnoughSafeUtxos = true)
+ log.info("commit-tx is unconfirmed, we'll spend the ephemeral anchor (targetFeerate={})", targetFeerate)
+ val (signedTx, remainingFee) = setPackageFee(txInfo, targetFeerate)
+ val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), s"mempool-tx-monitor-${signedTx.txid}")
+ txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), signedTx, parentTx_opt = Some(cmd.commitTx), cmd.input, nodeParams.channelConf.minDepth, cmd.desc, remainingFee)
+ wait(FundedTx(txInfo, walletInputs_opt = None, signedTx, targetFeerate))
+ }
+ }
+
+ private def setPackageFee(txInfo: ClaimRemoteMainOutputTx, targetFeerate: FeeratePerKw): (Transaction, Satoshi) = {
+ val commitFee = cmd.commitment.capacity - cmd.commitTx.txOut.map(_.amount).sum
+ if (Transactions.fee2rate(commitFee, cmd.commitTx.weight()) >= targetFeerate) {
+ // The commitment transaction already pays enough fees, so we don't take its weight into account.
+ val targetFee = Transactions.weight2fee(targetFeerate, txInfo.expectedWeight + cmd.commitment.commitmentFormat.anchorInputWeight)
+ val signedTx = txInfo.addSharedAnchorAndSign(targetFee, cmd.commitTx)
+ (signedTx, targetFee)
+ } else {
+ val packageWeight = cmd.commitTx.weight() + txInfo.expectedWeight + cmd.commitment.commitmentFormat.anchorInputWeight
+ val targetFee = Transactions.weight2fee(targetFeerate, packageWeight)
+ val missingFee = (targetFee - commitFee).max(0 sat)
+ val signedTx = txInfo.addSharedAnchorAndSign(missingFee, cmd.commitTx)
+ (signedTx, missingFee)
+ }
+ }
+
// Wait for our transaction to be confirmed or rejected from the mempool.
// If we get close to the confirmation target and our transaction is stuck in the mempool, we will initiate an RBF attempt.
private def wait(tx: FundedTx): Behavior[Command] = {
@@ -209,12 +249,19 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
val shouldRbf = cmd.txInfo match {
// We only need to increase fees on the anchor tx if the commit tx isn't confirmed.
case _: ClaimAnchorTx => !parentConfirmed
+ case _: ClaimRemoteMainOutputTx => !parentConfirmed
case _ => true
}
if (shouldRbf) {
- context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.safeUtxosThreshold)) {
- case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight)
- case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst
+ cmd.txInfo match {
+ case _: ClaimRemoteMainOutputTx =>
+ // We don't use wallet utxos in that case.
+ context.self ! CheckUtxosResult(isSafe = true, nodeParams.currentBlockHeight)
+ case _ =>
+ context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.safeUtxosThreshold)) {
+ case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight)
+ case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst
+ }
}
}
Behaviors.same
@@ -250,7 +297,11 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
// We avoid a herd effect whenever we fee bump transactions.
targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis))
Behaviors.same
- case BumpFee(targetFeerate) => fundReplacement(targetFeerate, tx)
+ case BumpFee(targetFeerate) =>
+ tx.txInfo match {
+ case txInfo: ClaimRemoteMainOutputTx => fundReplacementWithoutWalletInputs(targetFeerate, txInfo, tx)
+ case _ => fundReplacementWithWalletInputs(targetFeerate, tx)
+ }
case UpdateConfirmationTarget(target) =>
confirmationTarget = target
Behaviors.same
@@ -259,14 +310,14 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
// Fund a replacement transaction because our previous attempt seems to be stuck in the mempool.
- private def fundReplacement(targetFeerate: FeeratePerKw, previousTx: FundedTx): Behavior[Command] = {
+ private def fundReplacementWithWalletInputs(targetFeerate: FeeratePerKw, previousTx: FundedTx): Behavior[Command] = {
log.info("bumping {} fees: previous feerate={}, next feerate={}", cmd.desc, previousTx.feerate, targetFeerate)
val txFunder = context.spawn(ReplaceableTxFunder(nodeParams, bitcoinClient, txPublishContext), "tx-funder-rbf")
txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), Left(previousTx), cmd.commitTx, cmd.commitment, targetFeerate)
Behaviors.receiveMessagePartial {
case WrappedFundingResult(result) =>
result match {
- case success: ReplaceableTxFunder.TransactionReady => publishReplacement(previousTx, success.fundedTx)
+ case success: ReplaceableTxFunder.TransactionReady => publishReplacement(previousTx, success.fundedTx, success.fundedTx.fee)
case ReplaceableTxFunder.FundingFailed(_) =>
log.warn("could not fund {} replacement transaction (target feerate={})", cmd.desc, targetFeerate)
wait(previousTx)
@@ -287,17 +338,25 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}
+ // Increase the feerate of our transaction package because our previous attempt seems to be stuck in the mempool.
+ private def fundReplacementWithoutWalletInputs(targetFeerate: FeeratePerKw, txInfo: ClaimRemoteMainOutputTx, previousTx: FundedTx): Behavior[Command] = {
+ val (signedTx, remainingFee) = setPackageFee(txInfo, targetFeerate)
+ val bumpedTx = FundedTx(txInfo, walletInputs_opt = None, signedTx, targetFeerate)
+ publishReplacement(previousTx, bumpedTx, remainingFee)
+ }
+
// Publish an RBF attempt. We then have two concurrent transactions: the previous one and the updated one.
// Only one of them can be in the mempool, so we wait for the other to be rejected. Once that's done, we're back to a
// situation where we have one transaction in the mempool and wait for it to confirm.
- private def publishReplacement(previousTx: FundedTx, bumpedTx: FundedTx): Behavior[Command] = {
+ private def publishReplacement(previousTx: FundedTx, bumpedTx: FundedTx, bumpedFee: Satoshi): Behavior[Command] = {
val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), s"mempool-tx-monitor-${bumpedTx.signedTx.txid}")
val parentTx_opt = cmd.txInfo match {
// Anchor output transactions are packaged with the corresponding commitment transaction.
case _: ClaimAnchorTx => Some(cmd.commitTx)
+ case _: ClaimRemoteMainOutputTx => Some(cmd.commitTx)
case _ => None
}
- txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), bumpedTx.signedTx, parentTx_opt, cmd.input, nodeParams.channelConf.minDepth, cmd.desc, bumpedTx.fee)
+ txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), bumpedTx.signedTx, parentTx_opt, cmd.input, nodeParams.channelConf.minDepth, cmd.desc, bumpedFee)
Behaviors.receiveMessagePartial {
case WrappedTxResult(txResult) =>
txResult match {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
index d37e1c0..73b319f 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
@@ -226,7 +226,7 @@ class Peer(val nodeParams: NodeParams,
requireConfirmedInputs = requireConfirmedInputs,
requestFunding_opt = c.requestFunding_opt,
localChannelParams = localParams,
- proposedCommitParams = nodeParams.channelConf.commitParams(c.fundingAmount, unlimitedMaxHtlcValueInFlight = false),
+ proposedCommitParams = nodeParams.channelConf.commitParams(c.fundingAmount, channelType, unlimitedMaxHtlcValueInFlight = false),
remote = d.peerConnection,
remoteInit = d.remoteInit,
channelFlags = c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags),
@@ -290,7 +290,7 @@ class Peer(val nodeParams: NodeParams,
pushAmount_opt = None,
requireConfirmedInputs = false,
localChannelParams = localParams,
- proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingSatoshis, unlimitedMaxHtlcValueInFlight = false),
+ proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingSatoshis, channelType, unlimitedMaxHtlcValueInFlight = false),
remote = d.peerConnection,
remoteInit = d.remoteInit,
channelConfig = channelConfig,
@@ -305,7 +305,7 @@ class Peer(val nodeParams: NodeParams,
pushAmount_opt = None,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
localChannelParams = localParams,
- proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingAmount + addFunding_opt.map(_.fundingAmount).getOrElse(0 sat), unlimitedMaxHtlcValueInFlight = false),
+ proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingAmount + addFunding_opt.map(_.fundingAmount).getOrElse(0 sat), channelType, unlimitedMaxHtlcValueInFlight = false),
remote = d.peerConnection,
remoteInit = d.remoteInit,
channelConfig = channelConfig,
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
index 8793de7..7a3e6ec 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala
@@ -36,6 +36,7 @@ object CommitmentOutput {
case class ToRemote(txOut: TxOut) extends CommitmentOutput
case class ToLocalAnchor(txOut: TxOut) extends CommitmentOutput
case class ToRemoteAnchor(txOut: TxOut) extends CommitmentOutput
+ case class ToSharedAnchor(txOut: TxOut) extends CommitmentOutput
// If there is an output for an HTLC in the commit tx, there is also a 2nd-level HTLC tx.
case class InHtlc(htlc: IncomingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput
case class OutHtlc(htlc: OutgoingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput
@@ -94,6 +95,7 @@ final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: Feera
def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match {
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat)
+ case ZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => commitTxFeerate
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
index 61a38b7..ad6ffdb 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
@@ -37,6 +37,7 @@ import scala.util.{Success, Try}
object Scripts {
private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
+ case ZeroFeeCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
}
/** Sort public keys using lexicographic ordering. */
@@ -197,6 +198,7 @@ object Scripts {
def htlcOffered(keys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = {
val addCsvDelay = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true
+ case ZeroFeeCommitmentFormat => false
}
// @formatter:off
// To you with revocation key
@@ -255,6 +257,7 @@ object Scripts {
def htlcReceived(keys: CommitmentPublicKeys, paymentHash: ByteVector32, lockTime: CltvExpiry, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = {
val addCsvDelay = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true
+ case ZeroFeeCommitmentFormat => false
}
// @formatter:off
// To you with revocation key
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 a9b9c98..d53e614 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
@@ -81,7 +81,7 @@ object Transactions {
def claimHtlcTimeoutWeight: Int
/** Weight of a fully signed [[ClaimLocalDelayedOutputTx]] transaction. */
def toLocalDelayedWeight: Int
- /** Weight of a fully signed [[ClaimRemoteDelayedOutputTx]] transaction. */
+ /** Weight of a fully signed [[ClaimRemoteMainOutputTx]] transaction. */
def toRemoteWeight: Int
/** Weight of a fully signed [[HtlcDelayedTx]] 3rd-stage transaction (spending the output of an [[HtlcTx]]). */
def htlcDelayedWeight: Int
@@ -146,6 +146,33 @@ object Transactions {
override def toString: String = "anchor_outputs"
}
+ /**
+ * Commitment format that adds a shared anchor output (standard P2A script) to the commitment transaction and uses
+ * v3 (TRUC) transactions to allow CPFP and package relay.
+ */
+ case object ZeroFeeCommitmentFormat extends SegwitV0CommitmentFormat {
+ override val commitWeight: Int = 773
+ override val anchorInputWeight: Int = 164
+ override val htlcOutputWeight: Int = 172
+ override val htlcTimeoutInputWeight: Int = 449
+ override val htlcTimeoutWeight: Int = 663
+ override val htlcSuccessInputWeight: Int = 488
+ override val htlcSuccessWeight: Int = 703
+ override val claimHtlcSuccessWeight: Int = 571
+ override val claimHtlcTimeoutWeight: Int = 544
+ override val toLocalDelayedWeight: Int = 483
+ override val toRemoteWeight: Int = 438
+ override val htlcDelayedWeight: Int = 483
+ override val mainPenaltyWeight: Int = 483
+ override val htlcOfferedPenaltyWeight: Int = 572
+ override val htlcReceivedPenaltyWeight: Int = 577
+ override val claimHtlcPenaltyWeight: Int = 483
+ // The anchor output amount is capped at this value: afterwards, trimmed outputs go directly to fees.
+ val maxAnchorAmount: Satoshi = 240 sat
+
+ override def toString: String = "zero_fee_commitments"
+ }
+
sealed trait TaprootCommitmentFormat extends CommitmentFormat
sealed trait SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat {
@@ -190,6 +217,12 @@ object Transactions {
}
object RedeemInfo {
sealed trait SegwitV0 extends RedeemInfo { def redeemScript: ByteVector }
+ /** @param publicKey the public key for this p2wpkh input. */
+ case class P2wpkh(publicKey: PublicKey) extends SegwitV0 {
+ // This looks surprising at first glance, but p2pkh is the correct redeem script for p2wpkh outputs.
+ override val redeemScript: ByteVector = Script.write(Script.pay2pkh(publicKey))
+ override val pubkeyScript: ByteVector = Script.write(Script.pay2wpkh(publicKey))
+ }
/** @param redeemScript the actual script must be known to redeem pay2wsh inputs. */
case class P2wsh(redeemScript: ByteVector) extends SegwitV0 {
override val pubkeyScript: ByteVector = Script.write(Script.pay2wsh(redeemScript))
@@ -216,6 +249,10 @@ object Transactions {
val redeemScript: ByteVector = leaf.getScript
override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree))
}
+ /** Standard pay-to-anchor (P2A) output (introduced in https://github.com/bitcoin/bitcoin/pull/30352). */
+ case object PayToAnchor extends Taproot {
+ override val pubkeyScript: ByteVector = Script.write(Script.pay2anchor)
+ }
}
// @formatter:on
@@ -250,6 +287,9 @@ object Transactions {
tx.signInputTaprootKeyPath(key, inputIndex, spentOutputs, sighash, t.scriptTree_opt)
case s: RedeemInfo.TaprootScriptPath =>
tx.signInputTaprootScriptPath(key, inputIndex, spentOutputs, sighash, s.leafHash)
+ case RedeemInfo.PayToAnchor =>
+ // We never call this function for PayToAnchor inputs, which don't require a signature.
+ ByteVector64.Zeroes
}
}
@@ -265,6 +305,7 @@ object Transactions {
case s: RedeemInfo.TaprootScriptPath =>
val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, Seq(input.txOut), sighash, s.leafHash)
Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly)
+ case RedeemInfo.PayToAnchor => true
}
} else {
false
@@ -463,13 +504,13 @@ object Transactions {
* Transactions spending a remote [[CommitTx]] or one of its descendants.
*
* When a current remote [[CommitTx]] is published:
- * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends the to-local output of [[CommitTx]]
+ * - When using anchor outputs, [[ClaimRemoteMainOutputTx]] spends the to-local output of [[CommitTx]]
* - When using anchor outputs, [[ClaimRemoteAnchorTx]] spends the to-local anchor of [[CommitTx]]
* - [[ClaimHtlcSuccessTx]] spends received htlc outputs of [[CommitTx]] for which we have the preimage
* - [[ClaimHtlcTimeoutTx]] spends sent htlc outputs of [[CommitTx]] after a timeout
*
* When a revoked remote [[CommitTx]] is published:
- * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends the to-local output of [[CommitTx]]
+ * - When using anchor outputs, [[ClaimRemoteMainOutputTx]] spends the to-local output of [[CommitTx]]
* - [[MainPenaltyTx]] spends the remote main output using the revocation secret
* - [[HtlcPenaltyTx]] spends all htlc outputs using the revocation secret (and competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the remote node)
* - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] transactions published by the remote node using the revocation secret
@@ -508,7 +549,7 @@ object Transactions {
// @formatter:on
def sighash(txOwner: TxOwner): Int = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat => txOwner match {
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat => txOwner match {
case TxOwner.Local => SIGHASH_ALL
case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
}
@@ -592,7 +633,7 @@ object Transactions {
val htlc = output.htlc.add
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val tx = Transaction(
- version = 2,
+ version = commitTx.version,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
txOut = output.htlcDelayedOutput :: Nil,
lockTime = 0
@@ -601,7 +642,7 @@ object Transactions {
}
def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(htlcReceived(commitKeys, paymentHash, htlcExpiry, commitmentFormat))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -646,7 +687,7 @@ object Transactions {
val htlc = output.htlc.add
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val tx = Transaction(
- version = 2,
+ version = commitTx.version,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
txOut = output.htlcDelayedOutput :: Nil,
lockTime = htlc.cltvExpiry.toLong
@@ -655,7 +696,7 @@ object Transactions {
}
def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(htlcOffered(commitKeys, paymentHash, commitmentFormat))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -671,7 +712,7 @@ object Transactions {
override def sign(): Transaction = {
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay))
val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
witnessToLocalDelayedAfterDelay(sig, redeemScript)
@@ -694,7 +735,7 @@ object Transactions {
val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.htlcDelayedWeight)
val tx = Transaction(
- version = 2,
+ version = htlcTx.version,
txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0
@@ -705,7 +746,7 @@ object Transactions {
}
def redeemInfo(commitKeys: CommitmentPublicKeys, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys, toLocalDelay))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -731,7 +772,7 @@ object Transactions {
override def sign(): Transaction = {
// Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's an offered (outgoing) HTLC.
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat))
val sig = sign(commitKeys.ourHtlcKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
witnessClaimHtlcSuccessFromCommitTx(sig, preimage, redeemScript)
@@ -791,7 +832,7 @@ object Transactions {
override def sign(): Transaction = {
// Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's a received (incoming) HTLC.
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat))
val sig = sign(commitKeys.ourHtlcKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
witnessClaimHtlcTimeoutFromCommitTx(sig, redeemScript)
@@ -853,12 +894,16 @@ object Transactions {
commitmentFormat match {
case _: AnchorOutputsCommitmentFormat => RedeemInfo.P2wsh(anchor(fundingKey))
case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(paymentKey.xOnly, Some(Taproot.anchorScriptTree))
+ case ZeroFeeCommitmentFormat => RedeemInfo.PayToAnchor
}
}
- def createUnsignedTx(input: InputInfo): Transaction = {
+ def createUnsignedTx(input: InputInfo, commitmentFormat: CommitmentFormat): Transaction = {
Transaction(
- version = 2,
+ version = commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 2
+ case ZeroFeeCommitmentFormat => 3
+ },
txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil,
txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs
lockTime = 0
@@ -883,6 +928,7 @@ object Transactions {
val redeemInfo = RedeemInfo.TaprootKeyPath(anchorKey.xOnlyPublicKey(), Some(Taproot.anchorScriptTree))
val sig = toSign.sign(anchorKey, sighash, redeemInfo, walletInputs.spentUtxos)
Script.witnessKeyPathPay2tr(sig)
+ case ZeroFeeCommitmentFormat => Script.witnessPay2anchor
}
toSign.tx.updateWitness(toSign.inputIndex, witness)
}
@@ -899,7 +945,7 @@ object Transactions {
}
def createUnsignedTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimLocalAnchorTx] = {
- findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimLocalAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input), commitmentFormat))
+ findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimLocalAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input, commitmentFormat), commitmentFormat))
}
}
@@ -919,6 +965,7 @@ object Transactions {
val redeemInfo = RedeemInfo.TaprootKeyPath(commitKeys.ourPaymentKey.xOnlyPublicKey(), Some(Taproot.anchorScriptTree))
val sig = toSign.sign(commitKeys.ourPaymentKey, sighash, redeemInfo, walletInputs.spentUtxos)
Script.witnessKeyPathPay2tr(sig)
+ case ZeroFeeCommitmentFormat => Script.witnessPay2anchor
}
toSign.tx.updateWitness(toSign.inputIndex, witness)
}
@@ -935,13 +982,28 @@ object Transactions {
}
def createUnsignedTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteAnchorTx] = {
- findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimRemoteAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input), commitmentFormat))
+ findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimRemoteAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input, commitmentFormat), commitmentFormat))
}
}
- /** This transaction spends our main balance from the remote commitment with a 1-block relative delay. */
- case class ClaimRemoteDelayedOutputTx(commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction {
- override val desc: String = "remote-main-delayed"
+ /**
+ * This transaction spends our main balance from the remote commitment.
+ *
+ * When using [[AnchorOutputsCommitmentFormat]] or [[SimpleTaprootChannelCommitmentFormat]], there is a CSV-1 on the
+ * output to allow CPFP carve-out on the anchor outputs.
+ *
+ * Otherwise, it directly sends to a public key (p2wpkh or p2tr). In theory we could avoid making a 2nd-stage transaction
+ * (and directly use a public key from our bitcoin wallet), but it adds complexity and doesn't work in the case where
+ * we upgrade an anchor outputs channel to v3 during a splice (since the public key was generated when the channel was
+ * opened and cannot be changed afterwards). Another reason to use a 2nd-stage transaction is because that's how we
+ * pay the on-chain fees for the commitment transaction (which doesn't pay any fees) by also spending the P2A output
+ * (which avoids the need for external wallet inputs).
+ * The only case where it is wasteful to have this 2nd-stage transaction is if the remote peer paid the fees for the
+ * remote commitment and it confirmed: in that case, we haven't paid any on-chain fees yet for the force-close, and
+ * our peer has paid for the largest transaction, so it's fine even though it's not optimal.
+ */
+ case class ClaimRemoteMainOutputTx(commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction {
+ override val desc: String = "remote-main"
override val expectedWeight: Int = commitmentFormat.toRemoteWeight
override def sign(): Transaction = {
@@ -955,13 +1017,35 @@ object Transactions {
val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash())
val sig = sign(commitKeys.ourPaymentKey, sighash, redeemInfo, extraUtxos = Map.empty)
Script.witnessScriptPathPay2tr(redeemInfo.internalKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree)
+ case ZeroFeeCommitmentFormat =>
+ val redeemInfo = RedeemInfo.P2wpkh(commitKeys.ourPaymentKey.publicKey)
+ val sig = sign(commitKeys.ourPaymentKey, sighash, redeemInfo, extraUtxos = Map.empty)
+ Script.witnessPay2wpkh(redeemInfo.publicKey, encodeWitnessEcdsaSig(sig, SIGHASH_ALL))
}
tx.updateWitness(inputIndex, witness)
}
+
+ /** Add the shared P2A anchor to this transaction to pays fees for the parent commit tx. */
+ def addSharedAnchorAndSign(fee: Satoshi, commitTx: Transaction): Transaction = {
+ findPubKeyScriptIndex(commitTx, RedeemInfo.PayToAnchor.pubkeyScript) match {
+ case Left(_) => sign()
+ case Right(idx) =>
+ val dustLimit = Scripts.dustLimit(tx.txOut.head.publicKeyScript)
+ val anchorInput = InputInfo(OutPoint(commitTx, idx), commitTx.txOut(idx))
+ val amountOut = (amountIn + anchorInput.txOut.amount - fee).max(dustLimit)
+ val tx1 = tx.copy(
+ txIn = tx.txIn :+ TxIn(anchorInput.outPoint, ByteVector.empty, 0, Script.witnessPay2anchor),
+ txOut = Seq(tx.txOut.head.copy(amount = amountOut))
+ )
+ val redeemInfo = RedeemInfo.P2wpkh(commitKeys.ourPaymentKey.publicKey)
+ val sig = this.copy(tx = tx1).sign(commitKeys.ourPaymentKey, sighash, redeemInfo, Map(anchorInput.outPoint -> anchorInput.txOut))
+ tx1.updateWitness(inputIndex, Script.witnessPay2wpkh(redeemInfo.publicKey, encodeWitnessEcdsaSig(sig, SIGHASH_ALL)))
+ }
+ }
}
- object ClaimRemoteDelayedOutputTx {
- def createUnsignedTx(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = {
+ object ClaimRemoteMainOutputTx {
+ def createUnsignedTx(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteMainOutputTx] = {
val redeemInfo = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat =>
val redeemScript = Script.write(toRemoteDelayed(commitKeys.publicKeys))
@@ -969,19 +1053,27 @@ object Transactions {
case _: SimpleTaprootChannelCommitmentFormat =>
val scriptTree: ScriptTree.Leaf = Taproot.toRemoteScriptTree(commitKeys.publicKeys)
RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash())
+ case ZeroFeeCommitmentFormat =>
+ RedeemInfo.P2wpkh(commitKeys.ourPaymentKey.publicKey)
}
findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match {
case Left(skip) => Left(skip)
case Right(outputIndex) =>
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toRemoteWeight)
+ val csvDelay = commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 1
+ case ZeroFeeCommitmentFormat => 0
+ }
val tx = Transaction(
- version = 2,
- txIn = TxIn(input.outPoint, ByteVector.empty, 1) :: Nil,
+ // Note that we use the same nVersion field as the commit tx here: this allows us to use this transaction
+ // to also spend the anchor when using v3 to pay the commit fees.
+ version = commitTx.version,
+ txIn = TxIn(input.outPoint, ByteVector.empty, csvDelay) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0
)
- val unsignedTx = ClaimRemoteDelayedOutputTx(commitKeys, input, tx, commitmentFormat)
+ val unsignedTx = ClaimRemoteMainOutputTx(commitKeys, input, tx, commitmentFormat)
skipTxIfBelowDust(unsignedTx, localDustLimit)
}
}
@@ -994,7 +1086,7 @@ object Transactions {
override def sign(): Transaction = {
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay))
val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
witnessToLocalDelayedAfterDelay(sig, redeemScript)
@@ -1011,7 +1103,7 @@ object Transactions {
object ClaimLocalDelayedOutputTx {
def createUnsignedTx(commitKeys: LocalCommitmentKeys, commitTx: Transaction, localDustLimit: Satoshi, toLocalDelay: CltvExpiryDelta, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = {
val redeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -1042,7 +1134,7 @@ object Transactions {
override def sign(): Transaction = {
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay))
val sig = sign(revocationKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript)
@@ -1059,7 +1151,7 @@ object Transactions {
object MainPenaltyTx {
def createUnsignedTx(commitKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, commitTx: Transaction, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, MainPenaltyTx] = {
val redeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -1094,9 +1186,11 @@ object Transactions {
override def sign(): Transaction = {
val sig = sign(revocationKey, sighash, redeemInfo, extraUtxos = Map.empty)
val witness = redeemInfo match {
+ case RedeemInfo.P2wpkh(_) => Script.witnessPay2wpkh(revocationKey.publicKey, encodeWitnessEcdsaSig(sig, SIGHASH_ALL))
case RedeemInfo.P2wsh(redeemScript) => Scripts.witnessHtlcWithRevocationSig(commitKeys, sig, redeemScript)
case _: RedeemInfo.TaprootKeyPath => Script.witnessKeyPathPay2tr(sig, sighash)
case s: RedeemInfo.TaprootScriptPath => Script.witnessScriptPathPay2tr(s.internalKey, s.leaf, ScriptWitness(Seq(sig)), s.scriptTree)
+ case RedeemInfo.PayToAnchor => Script.witnessPay2anchor
}
tx.updateWitness(inputIndex, witness)
}
@@ -1116,7 +1210,7 @@ object Transactions {
case (paymentHash, htlcExpiry) =>
// We don't know if this was an incoming or outgoing HTLC, so we try both cases.
val (offered, received) = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
(RedeemInfo.P2wsh(Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat))),
RedeemInfo.P2wsh(Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat))))
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -1165,7 +1259,7 @@ object Transactions {
override def sign(): Transaction = {
val witness = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay))
val sig = sign(revocationKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty)
Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript)
@@ -1188,7 +1282,7 @@ object Transactions {
feerate: FeeratePerKw,
commitmentFormat: CommitmentFormat): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = {
val redeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay))
RedeemInfo.P2wsh(redeemScript)
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -1200,7 +1294,7 @@ object Transactions {
val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex))
val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcPenaltyWeight)
val tx = Transaction(
- version = 2,
+ version = htlcTx.version,
txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0
@@ -1214,7 +1308,6 @@ object Transactions {
// @formatter:off
sealed trait TxGenerationSkipped
case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" }
- private case object OutputAlreadyInWallet extends TxGenerationSkipped { override def toString = "output doesn't need to be claimed, it belongs to our bitcoin wallet (p2wpkh or p2tr)" }
case object AmountBelowDustLimit extends TxGenerationSkipped { override def toString = "amount is below dust limit" }
private case class CannotUpdateFee(txInfo: ForceCloseTransaction) extends TxGenerationSkipped { override def toString = s"cannot update fee for ${txInfo.desc} transactions" }
// @formatter:on
@@ -1237,7 +1330,8 @@ object Transactions {
def offeredHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = {
commitmentFormat match {
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit
- case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcTimeoutWeight)
+ case ZeroFeeCommitmentFormat => dustLimit
+ case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => dustLimit + weight2fee(feerate, commitmentFormat.htlcTimeoutWeight)
}
}
@@ -1255,7 +1349,8 @@ object Transactions {
def receivedHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = {
commitmentFormat match {
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit
- case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcSuccessWeight)
+ case ZeroFeeCommitmentFormat => dustLimit
+ case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => dustLimit + weight2fee(feerate, commitmentFormat.htlcSuccessWeight)
}
}
@@ -1290,6 +1385,7 @@ object Transactions {
// This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output.
val anchorsCost = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2
+ case ZeroFeeCommitmentFormat => 0 sat // the anchor amount will be taken from trimmed outputs
}
txFee + anchorsCost
}
@@ -1343,10 +1439,12 @@ object Transactions {
private def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors
+ case ZeroFeeCommitmentFormat => 0 // the 1-block delay is unnecessary for v3 transactions
}
def makeCommitTxOutputs(localFundingPublicKey: PublicKey,
remoteFundingPublicKey: PublicKey,
+ commitmentInput: InputInfo,
commitmentKeys: CommitmentPublicKeys,
payCommitTxFees: Boolean,
dustLimit: Satoshi,
@@ -1381,7 +1479,7 @@ object Transactions {
if (toLocalAmount >= dustLimit) {
val redeemInfo = commitmentFormat match {
- case _: AnchorOutputsCommitmentFormat =>
+ case _: AnchorOutputsCommitmentFormat | ZeroFeeCommitmentFormat =>
RedeemInfo.P2wsh(toLocalDelayed(commitmentKeys, toSelfDelay))
case _: SimpleTaprootChannelCommitmentFormat =>
val toLocalTree = Taproot.toLocalScriptTree(commitmentKeys, toSelfDelay)
@@ -1394,6 +1492,8 @@ object Transactions {
val redeemInfo = commitmentFormat match {
case _: AnchorOutputsCommitmentFormat =>
RedeemInfo.P2wsh(toRemoteDelayed(commitmentKeys))
+ case ZeroFeeCommitmentFormat =>
+ RedeemInfo.P2wpkh(commitmentKeys.remotePaymentPublicKey)
case _: SimpleTaprootChannelCommitmentFormat =>
val scripTree = Taproot.toRemoteScriptTree(commitmentKeys)
RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scripTree, scripTree.hash())
@@ -1411,6 +1511,13 @@ object Transactions {
val redeemInfo = ClaimRemoteAnchorTx.redeemInfo(remoteFundingPublicKey, commitmentKeys, commitmentFormat)
outputs.append(ToRemoteAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript)))
}
+ case ZeroFeeCommitmentFormat =>
+ // The shared anchor amount is usually 0 sat (ephemeral dust), but when outputs are trimmed, or millisatoshi
+ // amounts are truncated to satoshis, the corresponding value goes to the anchor output.
+ // However, we cap this value at 240 sat (which makes the output non-ephemeral) and let the remaining dust
+ // directly go to on-chain fees.
+ val anchorAmount = (commitmentInput.txOut.amount - outputs.map(_.txOut.amount).sum).min(ZeroFeeCommitmentFormat.maxAnchorAmount)
+ outputs.append(ToSharedAnchor(TxOut(anchorAmount, RedeemInfo.PayToAnchor.pubkeyScript)))
}
outputs.sortWith(CommitmentOutput.isLessThan).toSeq
@@ -1421,11 +1528,15 @@ object Transactions {
localPaymentBasePoint: PublicKey,
remotePaymentBasePoint: PublicKey,
localIsChannelOpener: Boolean,
+ commitmentFormat: CommitmentFormat,
outputs: Seq[CommitmentOutput]): CommitTx = {
val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsChannelOpener, localPaymentBasePoint, remotePaymentBasePoint)
val (sequence, lockTime) = encodeTxNumber(txNumber)
val tx = Transaction(
- version = 2,
+ version = commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 2
+ case ZeroFeeCommitmentFormat => 3
+ },
txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil,
txOut = outputs.map(_.txOut),
lockTime = lockTime
@@ -1550,7 +1661,7 @@ object Transactions {
val updatedTx = txInfo.tx.copy(txOut = txInfo.tx.txOut.headOption.map(_.copy(amount = txInfo.amountIn - fee)).toSeq)
txInfo match {
case txInfo: ClaimLocalDelayedOutputTx => Right(txInfo.copy(tx = updatedTx))
- case txInfo: ClaimRemoteDelayedOutputTx => Right(txInfo.copy(tx = updatedTx))
+ case txInfo: ClaimRemoteMainOutputTx => Right(txInfo.copy(tx = updatedTx))
// Anchor transaction don't have any output: wallet inputs must be used to pay fees.
case txInfo: ClaimAnchorTx => Left(CannotUpdateFee(txInfo))
// HTLC transactions are pre-signed, we can't update their fee by lowering the output amount.
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
index dbf45f2..47df80b 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala
@@ -83,6 +83,7 @@ private[channel] object ChannelCodecs5 {
.typecase(0x02, provide(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
.typecase(0x03, provide(Transactions.PhoenixSimpleTaprootChannelCommitmentFormat))
.typecase(0x04, provide(Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat))
+ .typecase(0x05, provide(Transactions.ZeroFeeCommitmentFormat))
// 0x00 was used for pre-anchor channels, which have been deprecated after eclair v0.13.1.
.typecase(0x00, fail[Transactions.CommitmentFormat](Err("some of your channels are not using anchor outputs: you must restart with your previous eclair version and close those channels before updating to this version of eclair (see the release notes for more details)")))
diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors-zero-fee-commitment-format.json b/eclair-core/src/test/resources/bolt3-tx-test-vectors-zero-fee-commitment-format.json
new file mode 100644
index 0000000..21cb15f
--- /dev/null
+++ b/eclair-core/src/test/resources/bolt3-tx-test-vectors-zero-fee-commitment-format.json
@@ -0,0 +1,373 @@
+{
+ "local_funding_priv": "8f567cb6382507019349a47623902aa65d7a142ac85462eeb63dc11799ac2bb9",
+ "remote_funding_priv": "4d22d96f0c0ccecffee4554d20ed43e51235917508ee292d281235bb7ebe0e3e",
+ "funding_txid": "4b70a2ee47b3005a6316ff87055e94c6b3d433d0fd3b384c9ecf7813843c1eae",
+ "funding_index": 1,
+ "funding_amount_satoshis": 10000000,
+ "commitment_number": 42,
+ "to_self_delay": 720,
+ "local_payment_basepoint_secret": "94f29d20a225ea2f7093331ba0f0f28a9382d8ed08e1fd121329925cd0c01b6d",
+ "local_delayed_payment_basepoint_secret": "e9d4e1935bf16e948d76ad007baf0646df023af38f41bcf2c8799336949d291e",
+ "local_htlc_basepoint_secret": "f699038ef4f95b6b16b22a5c04fcb3c508d68d02cd2f86cf197e0fac451681b0",
+ "per_commitment_point": "0275d12130c276b4274358a328901f8fc47e6c72629102e4b46c9f27dd2c1dda98",
+ "remote_payment_basepoint_secret": "580bff39085f3a6ae8b1f32905e67366c522ea8f2418391145b2e98f1a7cb3f2",
+ "remote_htlc_basepoint_secret": "32df9c4dd46ab6210e74e81e15282106f8db883f45674eabb3324166c6513062",
+ "revocation_basepoint": "026788d019ed90149cbc9aa5ff26dd7f1a6d3cd1bee8bf36cf7d8310fbd3606b14",
+ "payment_hash_to_preimage": {
+ "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f": "d3ad493d19860e4744491cf0795c0bc96a8e2a49ebb30afadae7a7d077aa93b2",
+ "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a": "108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa",
+ "1a04764fd402b5557cba89ec3c4d8931b0225d6436923c65c079ce64a55084c4": "ec8f390e2ba4b8d807f1f83d25892169aecf63222f895469d04b521ccb9809dc",
+ "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf": "792dc27fc2bb0512bf2a203153f01d64458ca1dc3aea76f95e95399440120374",
+ "10b879729e8ddd44f2cfcf3cad6d62be535ca74e293c5ed4a59bd0dcbdad7ca1": "c916e086a4cd7d40f198708aefadd31149da628f820ca2fc213af10f7668501c",
+ "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983": "5591b96c0a6a03f51c27bfa658149260bd2fe5e2ce83130ce50d0229a3a947c5"
+ },
+ "tests": [
+ {
+ "name": "Commitment transaction without HTLCs, both outputs untrimmed",
+ "dust_limit_satoshis": 500,
+ "to_local_msat": 8000000000,
+ "to_remote_msat": 2000000000,
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800300000000000000000451024e7380841e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152ea00127a0000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0400483045022100a05afcdfaf045a0a7b6adb194dcda430bb8e47db8c6d1536b2a94bd98fed77ed02206d580dcd5cba42aebed39515fbd00fdd90480825ae23cce87eef1f1711e2125e0148304502210094afa18972599f7a78b06467bd11d742875baf74aa9e516a775720564671fd8e02206b9ed5f48fb92a1c19543d67f29ee3bd43afee1ec1af6f7f9b4e3371beb8216201475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20"
+ },
+ {
+ "name": "Commitment transaction without HTLCs, one output trimmed below maximum anchor amount",
+ "dust_limit_satoshis": 330,
+ "to_local_msat": 9999800000,
+ "to_remote_msat": 200000,
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef8002c8000000000000000451024e73b895980000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0400483045022100a13c79500a9b30eba7af13418816b54aea1da0bdf41c0aa10f53a29017080d5602201a11bcc10f99c4334f778ea94e83db63d2409ff10e9e95b4260ac0dba27a490f014730440220706abbc90e9ab70a7e1f0f28b24baf2f105e49b7a41bf3b5e694f060806fc54402206020a5e51e437c027610a59f0252ac075dfb2b8e5b45f49149e9532935ba5e7a01475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20"
+ },
+ {
+ "name": "Commitment transaction without HTLCs, one output trimmed above maximum anchor amount",
+ "dust_limit_satoshis": 15000,
+ "to_local_msat": 9990000000,
+ "to_remote_msat": 10000000,
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef8002f0000000000000000451024e73706f980000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a040047304402204042ce57689bb7e52af7fb9ec28d6610674ce00e5e438bb1119acfb91aad6386022065de45c4fe52d86249d80397f2c85e143550cb14b3de0cd1e6a54d0622a2bbd4014830450221009fb4e444e9fe2d7db0f867704745c8ea2e4e1018b5986fd8cb9d9facdc1bb1be02202d6589a20ea8a8e3594eac3c99bad523139a36e313a66c6302e619786cb4239601475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20"
+ },
+ {
+ "name": "Commitment transaction with all HTLCs above dust limit",
+ "dust_limit_satoshis": 5000,
+ "to_local_msat": 7925000000,
+ "to_remote_msat": 1985000000,
+ "incoming_htlcs": [
+ {
+ "id": 1,
+ "amount_msat": 5000000,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920150
+ },
+ {
+ "id": 2,
+ "amount_msat": 5000000,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920150
+ },
+ {
+ "id": 5,
+ "amount_msat": 5000000,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920150
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 5,
+ "amount_msat": 25000000,
+ "payment_hash": "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983",
+ "cltv_expiry": 920141
+ },
+ {
+ "id": 8,
+ "amount_msat": 25000000,
+ "payment_hash": "10b879729e8ddd44f2cfcf3cad6d62be535ca74e293c5ed4a59bd0dcbdad7ca1",
+ "cltv_expiry": 920141
+ },
+ {
+ "id": 13,
+ "amount_msat": 25000000,
+ "payment_hash": "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983",
+ "cltv_expiry": 920141
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800900000000000000000451024e73881300000000000022002075254560bb02c207015847abfda36d3a1b882e78c3f04b08325aac21c53989dc881300000000000022002075254560bb02c207015847abfda36d3a1b882e78c3f04b08325aac21c53989dc881300000000000022002075254560bb02c207015847abfda36d3a1b882e78c3f04b08325aac21c53989dca8610000000000002200200963a1f3e47b8d2a35664f70de49d3331b4499da201004868a9104271f0cd93ba8610000000000002200205b138be4da633f087be191275d3ce30828ae32d1630853fcbf66d22ca6fe2636a8610000000000002200205b138be4da633f087be191275d3ce30828ae32d1630853fcbf66d22ca6fe2636e8491e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152ea08ed780000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a04004830450221008e951c6305b8d661de280234019d664d7a706b6dc22c23f5488b6b141480d1c202207975f2c477d6fb26ac51b9aa81c1d2b35bb724cebbf21a63e5d7ef3d6e273baf01483045022100ea978743401f2834d516ea434c0396c919e22465b4d2e359e61e6783535a89b30220475e2dbcc204016472b8eb9437e237a26ca102c1aa16812baa7ae522cc6291e801475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c0135010000000000000000018813000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100b82fff4e0652b305cc93f5f8e45362c55591c9ddb8e05a168c5df177ad8ff7ba022061827439423e660099d4c924257314656939b36180bf21b3a253bdd56f9f2b6b83473044022024832648de0dc603b955b7b6a8ada64cc305affaf7357b018f481fb7e0c48c8d0220716568bee37589c5b67c941768fb10e10f48c1cc66b1128664455b99749a7c2a0120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503560a0eb175ac686800000000",
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c0135020000000000000000018813000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100c25f58229c0f9f985ef85235877268aad5da9cdcbceeb4425e668c97df2bfc920220053bca5957e55cdaa0734ba9a1b7a3eb0bdeced1d4496196d9d9de8ff1b20d2e83483045022100d2f99843a7b29ec1b5044d2dc99f118d25157cdfc07213975a3e0bbc57f22e0a022000ac6b2dab53f34877c376d074d433c599721ea58c113bb7d2b7a540abbaab790120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503560a0eb175ac686800000000",
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c0135030000000000000000018813000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100948054ef219ec5af7494224f65ad6fab9cfd415d7260cb70a4719e99d421871f02205a41957a428b7820cef844c41cd06d8443f55953e000f4e40658175544a012ce83483045022100efab186c9f98ff2326c56d26e0a4f0f58afc94fc241faae4334a944d277d7f3802207a79eb3f17fa874eca67c4245b670e51ca8d89d3bc02b2a8712ec28e71479eba0120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503560a0eb175ac686800000000"
+ ],
+ "signed_htlc_timeout_txs": [
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c013504000000000000000001a861000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100f2890a34be31987e4cc3e17f71d5174bb85121e57bb0105e8aa6793f6f057a42022000d0e11ffb3217bb91f1b8babb43e2726889cf3281bfaffa3b3b1c9970fa71098347304402200373b0e0533fa0140ae10df40b93bb599920ce582d0a9d07398499554591659e02207f6ef44097b4e850bae0a4b5d6e0778a056e32ccd403ea88c62004d84e91a0af01008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914504170790db95d43716b136806e6a0fdf06e39e488ac68684d0a0e00",
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c013505000000000000000001a861000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a05004730440220450536f38c6fde3e8777ad9c6d0a505ff44812de86fc82f0f2a0f4ce11f6ac7a022053306d3739ddd8a5cd8e2acba63b2076927200701762b314fb83faafcfcc389383483045022100e506e7ccb18b31fd3785477a3cfd885d7c21bf6eb9f5368cb321913208ae8e31022074c186967bf90a5583b46f445a6cb12fb6f2bf83805e048b486b8da29a553a4601008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914488ed834d26f1a1dc5e3428e1e1a214f743e6a2488ac68684d0a0e00",
+ "03000000000101c3b4fad51418c874498af12060d49477c154845040e3584104ae641c542c013506000000000000000001a861000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500473044022061b73271a1b6b5a1bd8e15c58b08320c72da0a1db19493abbfc6f74b5fa80c2c022030ff309d3413b9661fc8f8f2b5fd14bc733c239cc7ba8b971fd562263e62bf3b8347304402207559852cd036af82658949c28e4922f7351756616f8d04703e8060889b4b484e022050d997e1684826dc61bf0d88d9d08c01b8fcf67958ac7a177c320a1a70305fb001008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914488ed834d26f1a1dc5e3428e1e1a214f743e6a2488ac68684d0a0e00"
+ ]
+ },
+ {
+ "name": "Commitment transaction with all HTLCs above dust limit and millisatoshi truncation",
+ "dust_limit_satoshis": 5000,
+ "to_local_msat": 7949998969,
+ "to_remote_msat": 1983999030,
+ "incoming_htlcs": [
+ {
+ "id": 7,
+ "amount_msat": 10000650,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920150
+ },
+ {
+ "id": 9,
+ "amount_msat": 6000320,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920150
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 5,
+ "amount_msat": 25000821,
+ "payment_hash": "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983",
+ "cltv_expiry": 920141
+ },
+ {
+ "id": 8,
+ "amount_msat": 25000210,
+ "payment_hash": "10b879729e8ddd44f2cfcf3cad6d62be535ca74e293c5ed4a59bd0dcbdad7ca1",
+ "cltv_expiry": 920141
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800703000000000000000451024e73701700000000000022002075254560bb02c207015847abfda36d3a1b882e78c3f04b08325aac21c53989dc102700000000000022002075254560bb02c207015847abfda36d3a1b882e78c3f04b08325aac21c53989dca8610000000000002200200963a1f3e47b8d2a35664f70de49d3331b4499da201004868a9104271f0cd93ba8610000000000002200205b138be4da633f087be191275d3ce30828ae32d1630853fcbf66d22ca6fe2636ff451e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152eaae4e790000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a040047304402207077d60a004f42171b99c66b1b6b1799910d8f2cb72b0ef6d6500cd2f937c28a022016464954f0c05ae083bf559bc603c89f014006c44258d054a06ded667166a1f701483045022100cb78620afa0efd40d480286b59545e2e00379e78f6fb8c31d0d3659880176ed102201218323fbc8e0cd1184c7a164fad4cecb841f3eb75a3fc7d11208acbcb66386501475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [
+ "0300000000010122daaec93bca3ee6bee9c78cf7885e538a56d0cbd63e3df086bbac22b4f5455d010000000000000000017017000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100d53dbf9e7479d48322eda78cd96c0d3d6a53c55c7c0d629a2b11e1b1a02d6b95022031a06dda013ee5a26db181552499816de0a9786c71cbefd1614f8b2e6bcb91318347304402204e4fd2e36058ed515f36cc609e636989fa89bf0b1d0e968fe642fcca7e2ceb1e022055a69e1e9be2799ae57ebae59792c9f50ca3943e6c154b4b3aabce6b080009300120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503560a0eb175ac686800000000",
+ "0300000000010122daaec93bca3ee6bee9c78cf7885e538a56d0cbd63e3df086bbac22b4f5455d020000000000000000011027000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a050047304402206556f3ae8fb62fd764f1a65fcc238bfc7cd629872c52ba7ee7b91e34aed4f9de022008545d9f5477371f940d8719270cc1b22d04497f53a97bf75dfb6a9ec5e5b49e8347304402205aa6b8143ef46e644157c4f4ad2ff439ae3c2e0cd76c2d3050aa6a1f5a31d33d022072f430949c2996233c5e832c3957f4f9bbf5586af3d62ebf3c0621cf92511b1a0120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503560a0eb175ac686800000000"
+ ],
+ "signed_htlc_timeout_txs": [
+ "0300000000010122daaec93bca3ee6bee9c78cf7885e538a56d0cbd63e3df086bbac22b4f5455d03000000000000000001a861000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a05004830450221009d775b6e196693171a41c69337de17bd6c7846dd677d0f9db8595faedb3f621f022060eb980ee28effaa38515b31691ec522fef90db0f6b83c07cf3cf3aebc1302c083483045022100e93e04871634a1eded25a6721bb6005787e92cfea09831f7af99433891d5373202204ec63ff8fd536518eca98e068d05d562d7be9465cb4d72a52a35308a4b3d843001008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914504170790db95d43716b136806e6a0fdf06e39e488ac68684d0a0e00",
+ "0300000000010122daaec93bca3ee6bee9c78cf7885e538a56d0cbd63e3df086bbac22b4f5455d04000000000000000001a861000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100c86ebf7f229be7b621a34a3fac39cfe20ea6b26e078eca782c136e635077b47a02205866ca91ef3eb8bc8dd8190c25d8a70dd101b067cc038d367ef2ee5a67c1beea8347304402203f81e6064b5a57bdc9a8759a0906dd8ee767063878f10133c435198c89c575a2022045d4e28f14aad0be1f03e43b3c6bd994d95b18b7ee979b4166c50659d4c02fa601008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914488ed834d26f1a1dc5e3428e1e1a214f743e6a2488ac68684d0a0e00"
+ ]
+ },
+ {
+ "name": "Commitment transaction with dust HTLCs below maximum anchor amount",
+ "dust_limit_satoshis": 1000,
+ "to_local_msat": 7979870000,
+ "to_remote_msat": 1950000000,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 100000,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 1,
+ "amount_msat": 49900000,
+ "payment_hash": "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983",
+ "cltv_expiry": 920125
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 10000000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920140
+ },
+ {
+ "id": 1,
+ "amount_msat": 130000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920140
+ },
+ {
+ "id": 2,
+ "amount_msat": 10000000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920140
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef8006e6000000000000000451024e73102700000000000022002081cd2904fc5581e6459d9375384af1223e6bd4fd112f3505e87b81e622a48cb7102700000000000022002081cd2904fc5581e6459d9375384af1223e6bd4fd112f3505e87b81e622a48cb7ecc20000000000002200207c2edfefe2690c02efde0c461b8a9283ed2a68968109306be4cb5fa848bd7a7630c11d0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152ea5ec3790000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0400483045022100cb7b6ae16b80c1ad74baea6f37be3581ca60915da75a03be3b377493f3359cc302203b591047f669cce31215c8a5a6039d5fd8dbfd8d6b53af39a619e6541d6bcf3101483045022100a661444a0800fe5e8bf9ecdef3631610e834485503b8e58d64d08ed5e88ebb2d0220248944d134cadf36cef90d05785ece9432727e81f0f66958a903baa0d69061cd01475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [
+ "03000000000101d8211cfec4c28b189edcf7b12b739bfbc0f77e44cc6fab67e50fa23cc78481c503000000000000000001ecc2000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100bbafdb3eeb4ab737859798753e0685f203294b6225c1300c9db6867a6f5fde180220769ca281abe51e93d707262ab3f45c54f5ec0ad3529836de0536b24e635e1c9f8347304402200f60ba0673e03a25cd8ab2f2d8fe4e0bc3ac7ffafe932e7a76afb3be7189b4f50220541a02e557d7ff2ccf0f5be4fa22291d07ecf67d8666dffba55e76ecbb5b9bf901205591b96c0a6a03f51c27bfa658149260bd2fe5e2ce83130ce50d0229a3a947c58b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914488ed834d26f1a1dc5e3428e1e1a214f743e6a2488527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae6775033d0a0eb175ac686800000000"
+ ],
+ "signed_htlc_timeout_txs": [
+ "03000000000101d8211cfec4c28b189edcf7b12b739bfbc0f77e44cc6fab67e50fa23cc78481c5010000000000000000011027000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a05004830450221009beeb028f6de6f925a986f3aaac7931bd46779b5a2dc8e82ae0509cf0174468002205a2761d34b276752f7f6e62c013c9509104a831df1f4b91a692bb8ad48285d3183483045022100c04dd0b08fd50bfc921de2a5d09840aab97b116e95ffd09cc11470404280be1202204aee1cd187f0074c5b80bae98981d232168daba5bde67adc7539b1a55a5a670901008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a91489baf9f76be304292b0314ffa9c61eb794b05dd188ac68684c0a0e00",
+ "03000000000101d8211cfec4c28b189edcf7b12b739bfbc0f77e44cc6fab67e50fa23cc78481c5020000000000000000011027000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a050047304402203a6ed1dc27298cfb030a5ff0f237b5288e263bd3cfe13ced890f3559e7c25db10220713167cc374f850a2f414dd8988f87cbfe978ba56b8c5cf690aa3c2d357a174c83483045022100c2f636b397f0fd6731b04323a0743aee902f01ac2e8bdac72548ab5380fecd0f02206ee5c57fac4c5255018bc687ae46a97a9781b7d87722d3c73b8bc93d951f713f01008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a91489baf9f76be304292b0314ffa9c61eb794b05dd188ac68684c0a0e00"
+ ]
+ },
+ {
+ "name": "Commitment transaction with similar dust HTLCs below maximum anchor amount",
+ "dust_limit_satoshis": 546,
+ "to_local_msat": 7999880000,
+ "to_remote_msat": 1999940000,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 30000,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 1,
+ "amount_msat": 30000,
+ "payment_hash": "72c9386ba5a9d97b821d855930236d39c48dab5b1c2efe9ada44e2fbadcff983",
+ "cltv_expiry": 920125
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 30000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 1,
+ "amount_msat": 30000,
+ "payment_hash": "1a04764fd402b5557cba89ec3c4d8931b0225d6436923c65c079ce64a55084c4",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 2,
+ "amount_msat": 30000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 3,
+ "amount_msat": 30000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef8003b4000000000000000451024e7344841e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152ea88117a0000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0400473044022034f22696dd7501d65fc117af3edcfed535eb301317fe35eef08658b571fca4d0022030ff6276cff9e22968f8b08f92a1d4f2c37fe4da29de095df4841d32bdaa501b014830450221009c820c376715329110045b3cf90f59838fd2f86b16774cb5ccccbd6a62830d9602205f2d970bef2589a5e37035937ab7db0461223e429b943b38ecf577e32b7b457201475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [],
+ "signed_htlc_timeout_txs": []
+ },
+ {
+ "name": "Commitment transaction with millisatoshi dust HTLCs adding to less than 1 satoshi",
+ "dust_limit_satoshis": 330,
+ "to_local_msat": 7999978526,
+ "to_remote_msat": 1999970475,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 29525,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 21474,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800334000000000000000451024e7362841e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152eaea117a0000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a040047304402201368dc415e11647edbfc1faaaccbe4717e5fdb88c4220a0bb19969781c9f757f02203fad3a7578fcfdc428b527b97f918b639ff07941b3b42c44f58b1364fdc57177014730440220338a338e0d477b63d2a7a00baca06c45b74bdd63d02e7d198cd8435180e9317e022014d0b95c9fde3409678417ee1a71d1d4d755f28db66a83eb10bea56de83d628d01475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [],
+ "signed_htlc_timeout_txs": []
+ },
+ {
+ "name": "Commitment transaction with millisatoshi dust HTLCs adding to 1 satoshi",
+ "dust_limit_satoshis": 330,
+ "to_local_msat": 7999978525,
+ "to_remote_msat": 1999970475,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 29525,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 21475,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800334000000000000000451024e7362841e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152eaea117a0000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a040047304402201368dc415e11647edbfc1faaaccbe4717e5fdb88c4220a0bb19969781c9f757f02203fad3a7578fcfdc428b527b97f918b639ff07941b3b42c44f58b1364fdc57177014730440220338a338e0d477b63d2a7a00baca06c45b74bdd63d02e7d198cd8435180e9317e022014d0b95c9fde3409678417ee1a71d1d4d755f28db66a83eb10bea56de83d628d01475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [],
+ "signed_htlc_timeout_txs": []
+ },
+ {
+ "name": "Commitment transaction with millisatoshi dust HTLCs adding to more than 1 satoshi",
+ "dust_limit_satoshis": 330,
+ "to_local_msat": 7999978288,
+ "to_remote_msat": 1999970247,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 29753,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 21712,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920125
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef800334000000000000000451024e7362841e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152eaea117a0000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a040047304402201368dc415e11647edbfc1faaaccbe4717e5fdb88c4220a0bb19969781c9f757f02203fad3a7578fcfdc428b527b97f918b639ff07941b3b42c44f58b1364fdc57177014730440220338a338e0d477b63d2a7a00baca06c45b74bdd63d02e7d198cd8435180e9317e022014d0b95c9fde3409678417ee1a71d1d4d755f28db66a83eb10bea56de83d628d01475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [],
+ "signed_htlc_timeout_txs": []
+ },
+ {
+ "name": "Commitment transaction with dust HTLCs above maximum anchor amount",
+ "dust_limit_satoshis": 2500,
+ "to_local_msat": 7988000000,
+ "to_remote_msat": 1988000000,
+ "incoming_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 2000000,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 1,
+ "amount_msat": 5000000,
+ "payment_hash": "ffa4f37b6d7dc03fecaaed8d36ebbea6d199e820e386e4549a69ce733f39f29f",
+ "cltv_expiry": 920130
+ },
+ {
+ "id": 2,
+ "amount_msat": 5000000,
+ "payment_hash": "23877c9093799487a8d49c7d6aaff7e06b9831c547400605252e7b07f7ae638a",
+ "cltv_expiry": 920130
+ }
+ ],
+ "outgoing_htlcs": [
+ {
+ "id": 0,
+ "amount_msat": 1000000,
+ "payment_hash": "1a04764fd402b5557cba89ec3c4d8931b0225d6436923c65c079ce64a55084c4",
+ "cltv_expiry": 920125
+ },
+ {
+ "id": 1,
+ "amount_msat": 1000000,
+ "payment_hash": "29a74a69c5941d402838f7e1a95c2b2ec534d79524b2582f48df7bc519ebaecf",
+ "cltv_expiry": 920130
+ },
+ {
+ "id": 2,
+ "amount_msat": 10000000,
+ "payment_hash": "10b879729e8ddd44f2cfcf3cad6d62be535ca74e293c5ed4a59bd0dcbdad7ca1",
+ "cltv_expiry": 920130
+ }
+ ],
+ "signed_commit_tx": "03000000000101ae1e3c841378cf9e4c383bfdd033d4b3c6945e0587ff16635a00b347eea2704b0100000000340fef8006f0000000000000000451024e7388130000000000002200202cb7b4a1ac1ca2aa06918f6933a9bf3a6ab777abfca18ac22fb4eeec587faf048813000000000000220020fd9f3b68e5a818a53f020ab09d1c956ead78f28b3c22950dc0d73568f797c92210270000000000002200200963a1f3e47b8d2a35664f70de49d3331b4499da201004868a9104271f0cd93ba0551e0000000000160014f2123f1a4b67887f2e5f02eda73e6327010152ea20e3790000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0400483045022100f706457b6f58634a64d86a403b622018d1cdf08064f08eaaf90ed47cbdfc81f002200b23d77b8151d0e907850d313b87cf53f0a1e8aef871cbb5ec3ef7f3af51326101483045022100cf3f7201781764693b8bb2db1a703c62fe9cb992103d4d950e9d1806e2e4c49202203993f5bfcc1f91617d48f22a10f2400ca298f42499d8389d4396bcad8750e88301475221027eb9596a68740445fb151ff37d5422e7f65f2c497c90fda63e738eb606c15bd62103bbc16dc8851bece603322f06b3c8da329401b7be7e9fdd3f3090ad19aed0807052aec50fbb20",
+ "signed_htlc_success_txs": [
+ "0300000000010142bb3321b077161ea560097e1b149edef504e00f1e2c6a77bbe4ad2aed6fde07010000000000000000018813000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500473044022072f786f54d93d9b1780bd3c605a434a2ae097852c6af75b18a23ea3e7cb995de02205608d310544bed398c406cf8f6cdeadbf119a8808c38415dbc93c82733168fc683483045022100e5e66a86053e6adda1b02786d9e8b9b7057026b8d81263fe5414d6b7660e70c7022055ff89300da995e2c8122e997338dac2d5ea2f526dcb243b9e7c1eb380155e100120d3ad493d19860e4744491cf0795c0bc96a8e2a49ebb30afadae7a7d077aa93b28b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a91416a189e9fcfe9edc8c6930da3dd87cebf44045a388527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503420a0eb175ac686800000000",
+ "0300000000010142bb3321b077161ea560097e1b149edef504e00f1e2c6a77bbe4ad2aed6fde07020000000000000000018813000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a05004730440220444371784d52a13dedec1baedd0a5f10cc78727a873614cf6981696e0ef9d28502202961336e7e0cbb530c4e5dda0a628bc21b5ce8e3f1e6274bd7ee20cf7b6974218347304402205b1693a32569a757d0928c163aae56a3986e3d53741ba0a9db5aac1289752aa3022002c8ef3e377a2461800e98cbfc40b31511d39e02473152943d959dcfd55e7c9e0120108cd7067c8ed6f3734b7b67ec153cfa83c40755b75c65e414e934099e6993aa8b76a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c8201208763a914f4c8e88504a23ed3e390ea80300c834f0eb79a6c88527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae677503420a0eb175ac686800000000"
+ ],
+ "signed_htlc_timeout_txs": [
+ "0300000000010142bb3321b077161ea560097e1b149edef504e00f1e2c6a77bbe4ad2aed6fde07030000000000000000011027000000000000220020f2d298ffcfd6d899a3abada37bfc6f42ce0b7b66f3e39e903e8419ac97dca75a0500483045022100eee7c19343ad58d9381a808fcd14af0c1e05bd56bd16c25c5ad8980af68b2df9022046acc611280afabcd82ef4a256d1f42be6618bc12d83bcd138141fb4915d256f83483045022100e6c8d81693cd3a03db0777b31e9318f8b8eb5f275817116dce8a72064d096bee02206439ddcac93a2756e1079e955245e2ac1924d874ba8388de9d497e4286cc20cb01008576a914ef8968bbfe34ad740642784d7b1efaebfd5b23ec8763ac672103d8507a026fb30bcd48ee9c765c7346470d0d397661d43dd2eb601f661ab92a0b7c820120876475527c2103c26117339025855b87deda5e138d438b2098881a5e6f81f72a60310faef473c652ae67a914504170790db95d43716b136806e6a0fdf06e39e488ac6868420a0e00"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala
index a7fa658..df5a53f 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala
@@ -128,7 +128,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice ! WatchFundingSpentTriggered(bobCommitTx)
// In response to that, alice publishes her claim txs.
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main")
val mainAmount = bobCommitTx.txOut(claimMain.input.index.toInt).amount
val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo)
assert(claimHtlcTxs.collect { case tx: ClaimHtlcSuccessTx => tx }.size == 1)
@@ -185,7 +185,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice ! WatchFundingSpentTriggered(bobCommitTx)
// In response to that, alice publishes her claim txs
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main")
val mainAmount = bobCommitTx.txOut(claimMain.input.index.toInt).amount
(1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
alice ! WatchTxConfirmedTriggered(BlockHeight(600_000), 5, bobCommitTx)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala
index 349b6d4..6393356 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala
@@ -114,7 +114,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat
val rcp = bob.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get
rcp.anchorOutput_opt.foreach(_ => bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx])
- rcp.localOutput_opt.foreach(_ => bob2blockchain.expectFinalTxPublished("remote-main-delayed"))
+ rcp.localOutput_opt.foreach(_ => bob2blockchain.expectFinalTxPublished("remote-main"))
// Bob is missing the preimage for 2 of the HTLCs she received.
assert(rcp.htlcOutputs.size == 6)
val claimHtlcTxs = (0 until 4).map(_ => bob2blockchain.expectMsgType[PublishReplaceableTx])
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala
index e3f8222..6ac9f58 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala
@@ -21,13 +21,13 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter
import akka.pattern.pipe
import akka.testkit.{TestFSMRef, TestProbe}
import com.softwaremill.quicklens.ModifyPimp
-import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId}
+import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, Script, ScriptElt, Transaction, TxId}
import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.MempoolTx
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCClient}
-import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, FeeratesPerKw}
+import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{CurrentBlockHeight, OnChainAddressCache}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
@@ -159,6 +159,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletA_opt = Some(walletClient))
val testTags = channelType match {
case _: ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputsPhoenix)
+ case _: ChannelTypes.ZeroFeeCommitments => Set(ChannelStateTestsTags.ZeroFeeCommitments, ChannelStateTestsTags.NoMaxHtlcValueInFlight)
case _ => Set.empty[String]
}
reachNormal(setup, testTags)
@@ -227,6 +228,33 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
}
}
+ test("commit tx feerate high enough, not spending anchor output (local commit, zero-fee commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ // We create many dust HTLCs, which will contribute to the commit feerate.
+ (1 to 10).foreach(_ => addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice))
+ crossSign(alice, bob, alice2bob, bob2alice)
+
+ // The current network feerate is lower than what the commit tx already pays.
+ setFeerate(FeeratePerByte(1 sat).perKw)
+ val commitTx = alice.signCommitTx()
+ assert(commitTx.txOut.exists(o => o.publicKeyScript == Script.write(Script.pay2anchor) && o.amount == ZeroFeeCommitmentFormat.maxAnchorAmount))
+ assert(Transactions.fee2rate(alice.commitments.latest.capacity - commitTx.txOut.map(_.amount).sum, commitTx.weight()) >= FeeratePerByte(1 sat).perKw)
+
+ probe.send(alice, CMD_FORCECLOSE(probe.ref))
+ probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]]
+ val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 20))
+ assert(anchorTx.commitTx == commitTx)
+ assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorTx])
+ publisher ! Publish(probe.ref, anchorTx)
+
+ val result = probe.expectMsgType[TxRejected]
+ assert(result.cmd == anchorTx)
+ assert(result.reason == TxSkipped(retryNextBlock = true))
+ }
+ }
+
test("commit tx feerate high enough, not spending anchor output (remote commit)") {
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
import f._
@@ -242,6 +270,38 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
}
}
+ test("commit tx feerate high enough, main output does not spend anchor output (remote commit, zero-fee commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ // We create many dust HTLCs, which will contribute to the commit feerate.
+ (1 to 10).foreach(_ => addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice))
+ crossSign(alice, bob, alice2bob, bob2alice)
+
+ // The current network feerate is lower than what the commit tx already pays.
+ setFeerate(FeeratePerByte(1 sat).perKw)
+ val commitTx = bob.signCommitTx()
+ assert(commitTx.txOut.exists(o => o.publicKeyScript == Script.write(Script.pay2anchor) && o.amount == ZeroFeeCommitmentFormat.maxAnchorAmount))
+ assert(Transactions.fee2rate(bob.commitments.latest.capacity - commitTx.txOut.map(_.amount).sum, commitTx.weight()) >= FeeratePerByte(1 sat).perKw)
+
+ probe.send(alice, WatchFundingSpentTriggered(commitTx))
+ val publishMain = alice2blockchain.expectMsgType[PublishReplaceableTx]
+ assert(publishMain.commitTx == commitTx)
+ assert(publishMain.txInfo.input.outPoint.txid == commitTx.txid)
+ assert(publishMain.txInfo.isInstanceOf[ClaimRemoteMainOutputTx])
+
+ val listener = TestProbe()
+ system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
+
+ publisher ! Publish(probe.ref, publishMain)
+ val mainTx = listener.expectMsgType[TransactionPublished].tx
+ assert(mainTx.version == 3)
+ assert(mainTx.txIn.map(_.outPoint.txid).toSet == Set(commitTx.txid))
+ val mempoolTxs = getMempoolTxs(2)
+ assert(mempoolTxs.map(_.txid).toSet == Set(commitTx.txid, mainTx.txid))
+ }
+ }
+
test("commit tx recently confirmed, not spending anchor output") {
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
import f._
@@ -457,6 +517,47 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
}
}
+ test("commit tx feerate too low, spending anchor output (local commit, zero-fee-commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ val commitTx = alice.signCommitTx()
+ probe.send(alice, CMD_FORCECLOSE(probe.ref))
+ probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]]
+
+ // Forward the anchor tx to the publisher.
+ val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 30))
+ assert(anchorTx.commitTx == commitTx)
+ assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorTx])
+
+ val targetFeerate = FeeratePerKw(3000 sat)
+ // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target what's provided.
+ setFeerate(targetFeerate, blockTarget = 12)
+ publisher ! Publish(probe.ref, anchorTx)
+
+ // wait for the commit tx and anchor tx to be published
+ val mempoolTxs = getMempoolTxs(2)
+ assert(mempoolTxs.map(_.txid).contains(commitTx.txid))
+
+ // we check that the anchor tx contains additional wallet inputs
+ // there are 2 transactions in the mempool, the one that is not the commit tx has to be the anchor tx
+ wallet.getTransaction(mempoolTxs.filterNot(_.txid == commitTx.txid).head.txid).pipeTo(probe.ref)
+ val publishedAnchorTx = probe.expectMsgType[Transaction]
+ assert(publishedAnchorTx.txIn.size > 1)
+
+ val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt)
+ val actualFee = mempoolTxs.map(_.fees).sum
+ assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee")
+
+ generateBlocks(6)
+ system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe)))
+ val result = probe.expectMsgType[TxConfirmed]
+ assert(result.cmd == anchorTx)
+ assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.txid))
+ assert(mempoolTxs.map(_.txid).contains(result.tx.txid))
+ }
+ }
+
private def testSpendRemoteCommitAnchor(f: Fixture, nextCommit: Boolean): Unit = {
import f._
@@ -630,6 +731,77 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
}
}
+ test("remote commit tx not published, publishing it and spending anchor output (zero-fee-commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ // Alice puts all of her balance in an HTLC to Bob: she won't have enough funds in her main output to pay the commit fee.
+ addHtlc(alice.commitments.availableBalanceForSend, alice, bob, alice2bob, bob2alice)
+ crossSign(alice, bob, alice2bob, bob2alice)
+ val commitTx = bob.signCommitTx()
+ probe.send(alice, WatchFundingSpentTriggered(commitTx))
+ val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx]
+ assert(publishAnchor.commitTx == commitTx)
+ assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.txid)
+ assert(publishAnchor.txInfo.isInstanceOf[ClaimRemoteAnchorTx])
+
+ val targetFeerate = FeeratePerKw(3000 sat)
+ setFeerate(targetFeerate)
+ val anchorTx = publishAnchor.copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 4))
+ publisher ! Publish(probe.ref, anchorTx)
+ // wait for the commit tx and anchor tx to be published
+ val mempoolTxs = getMempoolTxs(2)
+ assert(mempoolTxs.map(_.txid).contains(commitTx.txid))
+
+ val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt)
+ val actualFee = mempoolTxs.map(_.fees).sum
+ assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee")
+
+ generateBlocks(6)
+ system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe)))
+ val result = probe.expectMsgType[TxConfirmed]
+ assert(result.cmd == anchorTx)
+ assert(result.tx.version == 3)
+ assert(result.tx.txIn.count(_.outPoint.txid == commitTx.txid) == 1)
+ assert(mempoolTxs.map(_.txid).contains(result.tx.txid))
+ }
+ }
+
+ test("remote commit tx not published, publishing it and spending anchor output with main output (zero-fee-commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ val commitTx = bob.signCommitTx()
+ // When using zero-fee commitments, we use our main output to spend the anchor output.
+ probe.send(alice, WatchFundingSpentTriggered(commitTx))
+ val publishMain = alice2blockchain.expectMsgType[PublishReplaceableTx]
+ assert(publishMain.commitTx == commitTx)
+ assert(publishMain.txInfo.input.outPoint.txid == commitTx.txid)
+ assert(publishMain.txInfo.isInstanceOf[ClaimRemoteMainOutputTx])
+
+ val targetFeerate = FeeratePerKw(3000 sat)
+ setFeerate(targetFeerate)
+ val mainTx = publishMain.copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 6))
+ publisher ! Publish(probe.ref, mainTx)
+ // wait for the commit tx and main tx to be published
+ val mempoolTxs = getMempoolTxs(2)
+ assert(mempoolTxs.map(_.txid).contains(commitTx.txid))
+
+ val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt)
+ val actualFee = mempoolTxs.map(_.fees).sum
+ assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee")
+
+ generateBlocks(6)
+ system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe)))
+ val result = probe.expectMsgType[TxConfirmed]
+ assert(result.cmd == mainTx)
+ assert(result.tx.version == 3)
+ assert(result.tx.txIn.size == 2) // main output and anchor output
+ assert(result.tx.txIn.map(_.outPoint.txid).toSet == Set(commitTx.txid))
+ assert(mempoolTxs.map(_.txid).contains(result.tx.txid))
+ }
+ }
+
test("commit tx feerate too low, spending anchor outputs with multiple wallet inputs") {
val utxos = Seq(
// channel funding
@@ -727,6 +899,100 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
}
}
+ test("commit tx not confirming, lowering anchor output amount (zero-fee-commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ // Alice puts all of her balance in an HTLC to Bob: she won't have enough funds in her main output to pay the commit fee.
+ addHtlc(alice.commitments.availableBalanceForSend, alice, bob, alice2bob, bob2alice)
+ crossSign(alice, bob, alice2bob, bob2alice)
+ val commitTx = bob.signCommitTx()
+ probe.send(alice, WatchFundingSpentTriggered(commitTx))
+ val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx]
+ assert(publishAnchor.commitTx == commitTx)
+ assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.txid)
+ assert(publishAnchor.txInfo.isInstanceOf[ClaimRemoteAnchorTx])
+ assert(publishAnchor.confirmationTarget == ConfirmationTarget.Absolute(aliceBlockHeight() + 144)) // one outgoing HTLC that times out in 144 blocks
+
+ val listener = TestProbe()
+ system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
+
+ val oldFeerate = FeeratePerKw(3000 sat)
+ setFeerate(oldFeerate)
+ publisher ! Publish(probe.ref, publishAnchor)
+ // wait for the commit tx and anchor tx to be published
+ val anchorTx1 = listener.expectMsgType[TransactionPublished].tx
+ assert(anchorTx1.version == 3)
+ val mempoolTxs1 = getMempoolTxs(2)
+ assert(mempoolTxs1.map(_.txid).contains(commitTx.txid))
+ val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.txid).head
+ assert(mempoolAnchorTx1.txid == anchorTx1.txid)
+
+ // A new block is found, and the feerate has increased for our block target, so we bump the fees.
+ val newFeerate = FeeratePerKw(5000 sat)
+ setFeerate(newFeerate, blockTarget = 12)
+ system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 132))
+ val anchorTx2 = listener.expectMsgType[TransactionPublished].tx
+ assert(anchorTx2.version == 3)
+ assert(!isInMempool(mempoolAnchorTx1.txid))
+ val mempoolTxs2 = getMempoolTxs(2)
+ val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.txid).head
+ assert(mempoolAnchorTx2.txid == anchorTx2.txid)
+ assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees)
+
+ val targetFee = Transactions.weight2fee(newFeerate, mempoolTxs2.map(_.weight).sum.toInt)
+ val actualFee = mempoolTxs2.map(_.fees).sum
+ assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee")
+ }
+ }
+
+ test("commit tx not confirming, lowering main output amount (zero-fee-commitments)") {
+ withFixture(Seq(500 millibtc), ChannelTypes.ZeroFeeCommitments()) { f =>
+ import f._
+
+ val commitTx = bob.signCommitTx()
+ // When using zero-fee commitments, we use our main output to spend the anchor output.
+ probe.send(alice, WatchFundingSpentTriggered(commitTx))
+ val publishMainTx1 = alice2blockchain.expectMsgType[PublishReplaceableTx]
+ assert(publishMainTx1.commitTx == commitTx)
+ assert(publishMainTx1.txInfo.input.outPoint.txid == commitTx.txid)
+ assert(publishMainTx1.txInfo.isInstanceOf[ClaimRemoteMainOutputTx])
+
+ val listener = TestProbe()
+ system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
+
+ val oldFeerate = FeeratePerKw(3000 sat)
+ setFeerate(oldFeerate)
+ publisher ! Publish(probe.ref, publishMainTx1)
+ // wait for the commit tx and main tx to be published
+ val mainTx1 = listener.expectMsgType[TransactionPublished].tx
+ assert(mainTx1.version == 3)
+ assert(mainTx1.txIn.size == 2) // main output and anchor output
+ assert(mainTx1.txIn.map(_.outPoint.txid).toSet == Set(commitTx.txid))
+ val mempoolTxs1 = getMempoolTxs(2)
+ assert(mempoolTxs1.map(_.txid).contains(commitTx.txid))
+ val mempoolMainTx1 = mempoolTxs1.filter(_.txid != commitTx.txid).head
+ assert(mempoolMainTx1.txid == mainTx1.txid)
+
+ // A new block is found, and the feerate has increased for our block target, so we bump the fees.
+ val newFeerate = FeeratePerKw(5000 sat)
+ setFeerate(newFeerate, blockTarget = 12)
+ system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5))
+ val mainTx2 = listener.expectMsgType[TransactionPublished].tx
+ assert(mainTx2.version == 3)
+ assert(mainTx2.txIn.map(_.outPoint) == mainTx1.txIn.map(_.outPoint))
+ assert(!isInMempool(mainTx1.txid))
+ val mempoolTxs2 = getMempoolTxs(2)
+ val mempoolMainTx2 = mempoolTxs2.filter(_.txid != commitTx.txid).head
+ assert(mempoolMainTx2.txid == mainTx2.txid)
+ assert(mempoolMainTx1.fees < mempoolMainTx2.fees)
+
+ val targetFee = Transactions.weight2fee(newFeerate, mempoolTxs2.map(_.weight).sum.toInt)
+ val actualFee = mempoolTxs2.map(_.fees).sum
+ assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee")
+ }
+ }
+
test("commit tx not confirming, adding other wallet inputs") {
withFixture(Seq(10.2 millibtc, 0.15 millibtc, 0.15 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
import f._
@@ -1603,7 +1869,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
generateBlocks(1)
val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget))
assert(claimHtlcSuccess.txInfo.isInstanceOf[ClaimHtlcSuccessTx])
val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
index f68e53a..d52eed6 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
@@ -98,6 +98,8 @@ object ChannelStateTestsTags {
val OptionSimpleTaprootPhoenix = "option_simple_taproot_phoenix"
/** If set, channels will use taproot. */
val OptionSimpleTaproot = "option_simple_taproot"
+ /** If set, channel will use zero-fee commitments. */
+ val ZeroFeeCommitments = "zero_fee_commitments"
}
trait ChannelStateTestsBase extends Assertions with Eventually {
@@ -142,7 +144,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
dualFunded = dualFunded,
commitTxFeerate = channelType match {
case _: ChannelTypes.AnchorOutputs | ChannelTypes.SimpleTaprootChannelsPhoenix => TestConstants.phoenixCommitFeeratePerKw
- case _ => TestConstants.anchorOutputsFeeratePerKw
+ case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx | _: ChannelTypes.SimpleTaprootChannel => TestConstants.anchorOutputsFeeratePerKw
+ case _: ChannelTypes.ZeroFeeCommitments => FeeratePerKw(0 sat)
},
fundingTxFeerate = TestConstants.feeratePerKw,
fundingTxFeeBudget_opt = None,
@@ -265,6 +268,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(_.removed(Features.AnchorOutputsZeroFeeHtlcTx).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannels, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.removed(Features.SimpleTaprootChannels).updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional).updated(Features.PhoenixZeroReserve, FeatureSupport.Optional))
+ .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroFeeCommitments))(_.updated(Features.ZeroFeeCommitments, FeatureSupport.Optional))
)
val nodeParamsB1 = nodeParamsB.copy(features = nodeParamsB.features
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo))
@@ -277,6 +281,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(_.removed(Features.AnchorOutputsZeroFeeHtlcTx).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannels, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.removed(Features.SimpleTaprootChannels).updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional).updated(Features.PhoenixZeroReserve, FeatureSupport.Optional))
+ .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroFeeCommitments))(_.updated(Features.ZeroFeeCommitments, FeatureSupport.Optional))
)
(nodeParamsA1, nodeParamsB1)
}
@@ -287,7 +292,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
val zeroConf = canUse(Features.ZeroConf)
- if (canUse(Features.SimpleTaprootChannels)) {
+ if (canUse(Features.ZeroFeeCommitments)) {
+ ChannelTypes.ZeroFeeCommitments(scidAlias, zeroConf)
+ } else if (canUse(Features.SimpleTaprootChannels)) {
ChannelTypes.SimpleTaprootChannel(scidAlias, zeroConf)
} else if (canUse(Features.SimpleTaprootChannelsPhoenix)) {
ChannelTypes.SimpleTaprootChannelsPhoenix
@@ -636,12 +643,12 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
}
}
- case class PublishedForceCloseTxs(mainTx_opt: Option[Transaction], anchorTx: Transaction, htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction]) {
+ case class PublishedForceCloseTxs(mainTx_opt: Option[Transaction], anchorTx_opt: Option[Transaction], htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction]) {
val htlcTxs: Seq[Transaction] = htlcSuccessTxs ++ htlcTimeoutTxs
}
def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (LocalCommitPublished, PublishedForceCloseTxs) = {
- s ! Error(ByteVector32.Zeroes, "oops")
+ s ! CMD_FORCECLOSE(ActorRef.noSender, None, None)
eventually(assert(s.stateName == CLOSING))
val closingState = s.stateData.asInstanceOf[DATA_CLOSING]
assert(closingState.localCommitPublished.isDefined)
@@ -651,7 +658,11 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
assert(localCommitPublished.incomingHtlcs.size >= htlcSuccessCount)
assert(localCommitPublished.outgoingHtlcs.size == htlcTimeoutCount)
- val commitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx
+ val commitTx = closingState.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => s2blockchain.expectFinalTxPublished("commit-tx").tx
+ // In the 0-fee case, we cannot publish the commitment transaction alone.
+ case ZeroFeeCommitmentFormat => localCommitPublished.commitTx
+ }
assert(commitTx.txid == closingState.commitments.latest.localCommit.txId)
val commitInput = closingState.commitments.latest.commitInput(s.underlyingActor.channelKeys)
Transaction.correctlySpends(commitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -696,7 +707,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
})
// s is now in CLOSING state with txs pending for confirmation before going in CLOSED state
- val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs)
+ val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, Some(publishedAnchorTx), publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs)
(localCommitPublished, publishedTxs)
}
@@ -715,10 +726,17 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
assert(remoteCommitPublished.outgoingHtlcs.size == htlcTimeoutCount)
// If anchor outputs is used, we use the anchor output to bump the fees if necessary.
- val publishedAnchorTx = s2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx].tx
- // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed
- val publishedMainTx_opt = remoteCommitPublished.localOutput_opt.map(_ => s2blockchain.expectFinalTxPublished("remote-main-delayed").tx)
- publishedMainTx_opt.foreach(tx => Transaction.correctlySpends(tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
+ // When v3 transactions are used, we instead use our main output to bump the fees.
+ val (publishedMainTx_opt, publishedAnchorTx_opt) = closingData.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat =>
+ val publishedAnchorTx = s2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx].tx
+ val publishedMainTx_opt = remoteCommitPublished.localOutput_opt.map(_ => s2blockchain.expectFinalTxPublished("remote-main").tx)
+ publishedMainTx_opt.foreach(tx => Transaction.correctlySpends(tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
+ (publishedMainTx_opt, Some(publishedAnchorTx))
+ case ZeroFeeCommitmentFormat =>
+ val publishedMainTx = s2blockchain.expectReplaceableTxPublished[ClaimRemoteMainOutputTx].tx
+ (Some(publishedMainTx), None)
+ }
// all htlcs success/timeout should be claimed
val publishedClaimHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ =>
val claimHtlcTx = s2blockchain.expectMsgType[PublishReplaceableTx]
@@ -744,18 +762,18 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
// we watch outputs of the commitment tx that we want to claim
remoteCommitPublished.localOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint))
- remoteCommitPublished.anchorOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint))
+ publishedAnchorTx_opt.map(_.txIn.head.outPoint).foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint))
s2blockchain.expectWatchOutputsSpent(remoteCommitPublished.htlcOutputs.toSeq)
s2blockchain.expectNoMessage(100 millis)
// once our closing transactions are published, we watch for their confirmation
- (publishedMainTx_opt ++ Seq(publishedAnchorTx) ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => {
+ (publishedMainTx_opt ++ publishedAnchorTx_opt.toSeq ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => {
s ! WatchOutputSpentTriggered(tx.txOut.headOption.map(_.amount).getOrElse(330 sat), tx)
s2blockchain.expectWatchTxConfirmed(tx.txid)
})
// s is now in CLOSING state with txs pending for confirmation before going in CLOSED state
- val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs)
+ val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx_opt, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs)
(remoteCommitPublished, publishedTxs)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala
index 94e0953..44747dd 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala
@@ -138,11 +138,20 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
test("recv AcceptChannel (invalid max accepted htlcs)") { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptChannel]
- // spec says max = 483
- val invalidMaxAcceptedHtlcs = 484
- alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
+ alice ! accept.copy(maxAcceptedHtlcs = 484)
val error = alice2bob.expectMsgType[Error]
- assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage))
+ assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, maxAcceptedHtlcs = 484, max = 483).getMessage))
+ listener.expectMsgType[ChannelAborted]
+ awaitCond(alice.stateName == CLOSED)
+ aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected]
+ }
+
+ test("recv AcceptChannel (invalid max accepted htlcs, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val accept = bob2alice.expectMsgType[AcceptChannel]
+ alice ! accept.copy(maxAcceptedHtlcs = 115)
+ val error = alice2bob.expectMsgType[Error]
+ assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, maxAcceptedHtlcs = 115, max = 114).getMessage))
listener.expectMsgType[ChannelAborted]
awaitCond(alice.stateName == CLOSED)
aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala
index 0ab2f06..3745061 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala
@@ -181,10 +181,20 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
test("recv AcceptDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
- val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1
- alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
+ alice ! accept.copy(maxAcceptedHtlcs = 484)
val error = alice2bob.expectMsgType[Error]
- assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage))
+ assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, maxAcceptedHtlcs = 484, max = 483).getMessage))
+ listener.expectMsgType[ChannelAborted]
+ awaitCond(alice.stateName == CLOSED)
+ aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected]
+ }
+
+ test("recv AcceptDualFundedChannel (invalid max accepted htlcs, zero-fee commitments)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
+ alice ! accept.copy(maxAcceptedHtlcs = 115)
+ val error = alice2bob.expectMsgType[Error]
+ assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, maxAcceptedHtlcs = 115, max = 114).getMessage))
listener.expectMsgType[ChannelAborted]
awaitCond(alice.stateName == CLOSED)
aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala
index 84054ed..81bd7ec 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala
@@ -19,11 +19,11 @@ package fr.acinq.eclair.channel.states.a
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong, TxId}
import fr.acinq.eclair.TestConstants.Bob
-import fr.acinq.eclair.blockchain.fee.FeeratePerKw
+import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
-import fr.acinq.eclair.transactions.Transactions.{ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
+import fr.acinq.eclair.transactions.Transactions.{ZeroFeeCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, OpenChannel, TlvStream}
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@@ -97,6 +97,28 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
awaitCond(bob.stateName == CLOSED)
}
+ test("recv OpenChannel (zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val open = alice2bob.expectMsgType[OpenChannel]
+ assert(open.channelType_opt.contains(ChannelTypes.ZeroFeeCommitments()))
+ assert(open.feeratePerKw == FeeratePerKw(0 sat))
+ alice2bob.forward(bob)
+ awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
+ assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == ZeroFeeCommitmentFormat)
+ }
+
+ test("recv OpenChannel (zero-fee commitments, non-zero feerate)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val open = alice2bob.expectMsgType[OpenChannel]
+ assert(open.channelType_opt.contains(ChannelTypes.ZeroFeeCommitments()))
+ assert(open.feeratePerKw == FeeratePerKw(0 sat))
+ alice2bob.forward(bob, open.copy(feeratePerKw = FeeratePerByte(1 sat).perKw))
+ val error = bob2alice.expectMsgType[Error]
+ assert(error == Error(open.temporaryChannelId, FeerateTooDifferent(open.temporaryChannelId, FeeratePerKw(0 sat), FeeratePerByte(1 sat).perKw).getMessage))
+ listener.expectMsgType[ChannelAborted]
+ awaitCond(bob.stateName == CLOSED)
+ }
+
test("recv OpenChannel (invalid chain)") { f =>
import f._
val open = alice2bob.expectMsgType[OpenChannel]
@@ -147,10 +169,19 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
test("recv OpenChannel (invalid max accepted htlcs)") { f =>
import f._
val open = alice2bob.expectMsgType[OpenChannel]
- val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1
- bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
+ bob ! open.copy(maxAcceptedHtlcs = 484)
+ val error = bob2alice.expectMsgType[Error]
+ assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, maxAcceptedHtlcs = 484, max = 483).getMessage))
+ listener.expectMsgType[ChannelAborted]
+ awaitCond(bob.stateName == CLOSED)
+ }
+
+ test("recv OpenChannel (invalid max accepted htlcs, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val open = alice2bob.expectMsgType[OpenChannel]
+ alice2bob.forward(bob, open.copy(maxAcceptedHtlcs = 115))
val error = bob2alice.expectMsgType[Error]
- assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage))
+ assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, maxAcceptedHtlcs = 115, max = 114).getMessage))
listener.expectMsgType[ChannelAborted]
awaitCond(bob.stateName == CLOSED)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala
index 07b7ae9..eeb3ac8 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala
@@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states.a
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, SatoshiLong}
import fr.acinq.eclair.TestConstants.Bob
+import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
@@ -91,6 +92,31 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
}
+ test("recv OpenDualFundedChannel (zero-fee commitments)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+
+ val open = alice2bob.expectMsgType[OpenDualFundedChannel]
+ assert(open.channelType_opt.contains(ChannelTypes.ZeroFeeCommitments()))
+ assert(open.fundingFeerate == TestConstants.feeratePerKw)
+ assert(open.commitmentFeerate == FeeratePerKw(0 sat))
+ alice2bob.forward(bob)
+ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
+ assert(accept.channelType_opt.contains(ChannelTypes.ZeroFeeCommitments()))
+ awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
+ }
+
+ test("recv OpenDualFundedChannel (zero-fee commitments, non-zero feerate)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+
+ val open = alice2bob.expectMsgType[OpenDualFundedChannel]
+ assert(open.channelType_opt.contains(ChannelTypes.ZeroFeeCommitments()))
+ alice2bob.forward(bob, open.copy(commitmentFeerate = FeeratePerByte(1 sat).perKw))
+ val error = bob2alice.expectMsgType[Error]
+ assert(error == Error(open.temporaryChannelId, FeerateTooDifferent(open.temporaryChannelId, FeeratePerKw(0 sat), FeeratePerByte(1 sat).perKw).getMessage))
+ bobListener.expectMsgType[ChannelAborted]
+ awaitCond(bob.stateName == CLOSED)
+ }
+
test("recv OpenDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f =>
import f._
@@ -172,10 +198,19 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur
test("recv OpenDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._
val open = alice2bob.expectMsgType[OpenDualFundedChannel]
- val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1
- bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
+ bob ! open.copy(maxAcceptedHtlcs = 484)
+ val error = bob2alice.expectMsgType[Error]
+ assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, maxAcceptedHtlcs = 484, max = 483).getMessage))
+ bobListener.expectMsgType[ChannelAborted]
+ awaitCond(bob.stateName == CLOSED)
+ }
+
+ test("recv OpenDualFundedChannel (invalid max accepted htlcs, zero-fee commitments)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val open = alice2bob.expectMsgType[OpenDualFundedChannel]
+ bob ! open.copy(maxAcceptedHtlcs = 115)
val error = bob2alice.expectMsgType[Error]
- assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage))
+ assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, maxAcceptedHtlcs = 115, max = 114).getMessage))
bobListener.expectMsgType[ChannelAborted]
awaitCond(bob.stateName == CLOSED)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
index 41ebb3b..064f9f1 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
@@ -774,7 +774,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
alice ! WatchFundingSpentTriggered(bobCommitTx)
aliceListener.expectMsgType[TransactionPublished]
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice2blockchain.expectWatchOutputSpent(claimMain.input)
@@ -810,7 +810,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
alice ! WatchFundingSpentTriggered(bobCommitTx1)
assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == bobCommitTx1.txid)
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid)
alice2blockchain.expectWatchOutputSpent(claimMain.input)
@@ -833,7 +833,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid)
alice2 ! WatchFundingSpentTriggered(bobCommitTx)
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice2blockchain.expectWatchOutputSpent(claimMainAlice.input)
@@ -844,7 +844,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid)
bob2 ! WatchFundingSpentTriggered(aliceCommitTx)
bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid)
bob2blockchain.expectWatchOutputSpent(claimMainBob.input)
@@ -872,7 +872,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId))
alice2 ! WatchFundingSpentTriggered(bobCommitTx1)
alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid)
alice2blockchain.expectWatchOutputSpent(claimMainAlice.input)
@@ -885,7 +885,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
bob2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId))
bob2 ! WatchFundingSpentTriggered(aliceCommitTx1)
bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
bob2blockchain.expectWatchTxConfirmed(aliceCommitTx1.txid)
bob2blockchain.expectWatchOutputSpent(claimMainBob.input)
@@ -1338,7 +1338,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val bobCommitTx = bob.signCommitTx()
alice ! WatchFundingSpentTriggered(bobCommitTx)
val anchorTxRemote = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice2blockchain.expectWatchOutputSpent(claimMainRemote.input)
@@ -1386,7 +1386,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
// Bob publishes his commit tx, Alice reacts by spending her remote main output.
alice ! WatchFundingSpentTriggered(bobCommitTx1)
val anchorRemote = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid)
alice2blockchain.expectWatchOutputsSpent(Seq(anchorRemote.input.outPoint, claimMainRemote.input))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
index 053d392..182c627 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
@@ -3564,7 +3564,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Bob detects Alice's commit tx.
bob ! WatchFundingSpentTriggered(commitTx2)
val anchorBob = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main")
val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
bob2blockchain.expectWatchTxConfirmed(commitTx2.txid)
@@ -3676,7 +3676,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// We're back to the normal handling of remote commit.
val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main")
val htlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
htlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
// NB: this one fires immediately, tx is already confirmed.
@@ -3762,7 +3762,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Bob's revoked commit tx wins.
alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx)
// Alice reacts by punishing bob.
- val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
@@ -3866,7 +3866,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1)
// we're back to the normal handling of remote commit
val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main")
val claimHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
claimHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
awaitCond(aliceWallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid))
@@ -3966,7 +3966,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// bob's revoked tx wins
alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx)
// alice reacts by punishing bob
- val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
alice2blockchain.expectWatchTxConfirmed(bobRevokedCommitTx.txid)
@@ -4029,7 +4029,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Bob detects Alice's commit tx.
bob ! WatchFundingSpentTriggered(commitTx2)
val bobAnchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(claimMainBob.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
@@ -4122,7 +4122,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Alice's commit tx confirms.
bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx)
val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
@@ -4165,7 +4165,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Alice's commit tx confirms.
bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx)
- val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty")
Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -4202,7 +4202,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
crossSign(alice, bob, alice2bob, bob2alice)
bob ! WatchFundingSpentTriggered(aliceCommitTx)
// Bob reacts by publishing penalty transactions.
- val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty")
Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -4271,7 +4271,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Alice's revoked commit tx confirms.
bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx)
- val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = bob2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty")
Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala
index e965e02..a372127 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala
@@ -1102,6 +1102,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testRecvCommitSigMultipleHtlcZeroFees(f)
}
+ test("recv CommitSig (multiple htlcs in both directions, zero fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testRecvCommitSigMultipleHtlcZeroFees(f)
+ }
+
test("recv CommitSig (multiple htlcs in both directions) (without fundingTxId tlv)") { f =>
import f._
@@ -1642,6 +1646,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testRevokeAndAckHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv RevokeAndAck (one htlc sent, zero_fee_commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testRevokeAndAckHtlc(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv RevocationTimeout") { f =>
import f._
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
@@ -1685,6 +1693,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testReceiveCmdFulfillHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv CMD_FULFILL_HTLC (zero_fee_commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testReceiveCmdFulfillHtlc(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f =>
import f._
val sender = TestProbe()
@@ -1857,6 +1869,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testCmdFailHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv CMD_FAIL_HTLC (zero_fee_commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testCmdFailHtlc(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv CMD_FAIL_HTLC (with delay)") { f =>
import f._
val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
@@ -2158,6 +2174,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectWatchTxConfirmed(tx.txid)
}
+ test("recv UpdateFee (zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ val tx = bob.signCommitTx()
+ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
+ bob ! UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw)
+ bob2alice.expectMsgType[Error]
+ awaitCond(bob.stateName == CLOSING)
+ }
+
test("recv UpdateFee (sender can't afford it)", Tag(ChannelStateTestsTags.HighFeerateMismatchTolerance)) { f =>
import f._
val tx = bob.signCommitTx()
@@ -2740,6 +2765,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectNoMessage(100 millis)
}
+ test("recv CurrentFeerate (when funder, doesn't trigger and UpdateFee with zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+ assert(alice.commitments.latest.localCommit.spec.commitTxFeerate == FeeratePerKw(0 sat))
+ val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(5000 sat)).copy(minimum = FeeratePerKw(250 sat)))
+ alice.setBitcoinCoreFeerates(event.feeratesPerKw)
+ alice ! event
+ alice2bob.expectNoMessage(100 millis)
+ }
+
test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { f =>
import f._
val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(11000 sat)))
@@ -2803,7 +2837,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// in response to that, alice publishes her claim txs
val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main").tx
// in addition to her main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage
val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx])
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
@@ -2890,7 +2924,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// in response to that, alice publishes her claim txs
val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main").tx
// in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage
val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx])
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
@@ -2969,7 +3003,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
assert(rvk.htlcOutputs.size == 4)
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main").tx
val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx
val htlcPenaltyTxs = (0 until 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx)
// let's make sure that htlc-penalty txs each spend a different output
@@ -3025,7 +3059,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main").tx
val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx
val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx)
// let's make sure that htlc-penalty txs each spend a different output
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala
index 7a9680d..748d15a 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala
@@ -684,7 +684,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// in response to that, alice publishes her claim txs
val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val claimMain = alice2blockchain.expectFinalTxPublished("remote-main")
// in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage
val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => {
@@ -730,7 +730,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// in response to that, alice publishes her claim txs
val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
val claimTxs = Seq(
- alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx,
+ alice2blockchain.expectFinalTxPublished("remote-main").tx,
// there is only one htlc to claim in the commitment bob published
alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx].sign()
)
@@ -766,7 +766,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main").tx
val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx
val htlc1PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx
val htlc2PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx
@@ -804,7 +804,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main").tx
val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx
val htlcPenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx
Seq(mainTx, mainPenaltyTx, htlcPenaltyTx).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala
index 652a60f..327881c 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala
@@ -395,8 +395,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob claims the htlc output from Alice's commit tx using its preimage.
bob ! WatchFundingSpentTriggered(lcp.commitTx)
- bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- bob2blockchain.expectFinalTxPublished("remote-main-delayed")
+ bob.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat =>
+ bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
+ bob2blockchain.expectFinalTxPublished("remote-main")
+ case ZeroFeeCommitmentFormat =>
+ // We don't need a dedicated anchor transaction, we'll use our main output instead.
+ bob2blockchain.expectReplaceableTxPublished[ClaimRemoteMainOutputTx]
+ }
val claimHtlcSuccessTx1 = bob2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx].sign()
val claimHtlcSuccessTx2 = bob2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx].sign()
assert(Seq(claimHtlcSuccessTx1, claimHtlcSuccessTx2).flatMap(_.txIn.map(_.outPoint)).toSet == lcp.htlcOutputs)
@@ -425,6 +431,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
extractPreimageFromClaimHtlcSuccess(f)
}
+ test("recv WatchOutputSpentTriggered (extract preimage from Claim-HTLC-success tx, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ extractPreimageFromClaimHtlcSuccess(f)
+ }
+
private def extractPreimageFromHtlcSuccess(f: FixtureParam): Unit = {
import f._
@@ -465,6 +475,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
extractPreimageFromHtlcSuccess(f)
}
+ test("recv WatchOutputSpentTriggered (extract preimage from HTLC-success tx, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ extractPreimageFromHtlcSuccess(f)
+ }
+
private def extractPreimageFromRemovedHtlc(f: FixtureParam): Unit = {
import f._
@@ -500,13 +514,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Alice prepares Claim-HTLC-timeout transactions for each HTLC.
alice ! WatchFundingSpentTriggered(rcp.commitTx)
- val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val (mainTx_opt, anchorTx_opt) = alice.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat =>
+ val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx].tx
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main").tx
+ (Some(mainTx), Some(anchorTx))
+ case ZeroFeeCommitmentFormat =>
+ val mainTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteMainOutputTx].tx
+ (Some(mainTx), None)
+ }
val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
assert(claimHtlcTimeoutTxs.map(_.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id))
alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid)
- alice2blockchain.expectWatchOutputSpent(mainTx.input)
- alice2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint)
+ mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint))
+ anchorTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint))
alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint))
alice2blockchain.expectNoMessage(100 millis)
@@ -546,6 +567,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
extractPreimageFromRemovedHtlc(f)
}
+ test("recv WatchOutputSpentTriggered (extract preimage for removed HTLC, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ extractPreimageFromRemovedHtlc(f)
+ }
+
private def extractPreimageFromNextHtlcs(f: FixtureParam): Unit = {
import f._
@@ -587,7 +612,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Alice prepares Claim-HTLC-timeout transactions for each HTLC.
alice ! WatchFundingSpentTriggered(rcp.commitTx)
val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
assert(claimHtlcTimeoutTxs.map(_.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id))
alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid)
@@ -714,6 +739,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testLocalCommitTxConfirmed(f, PhoenixSimpleTaprootChannelCommitmentFormat)
}
+ test("recv WatchTxConfirmedTriggered (local commit, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testLocalCommitTxConfirmed(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv WatchTxConfirmedTriggered (local commit with multiple htlcs for the same payment)") { f =>
import f._
@@ -1014,7 +1043,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].input.outPoint == htlcTimeoutTx.txIn.head.outPoint)
alice2blockchain.expectWatchTxConfirmed(closingState.commitTx.txid)
closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint))
- alice2blockchain.expectWatchOutputSpent(closingTxs.anchorTx.txIn.head.outPoint)
+ closingTxs.anchorTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint))
alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.txIn.head.outPoint)
// the htlc transaction confirms, so we publish a 3rd-stage transaction
@@ -1061,7 +1090,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// The commit tx confirms.
alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState1.commitTx)
- alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingTxs.anchorTx)
+ closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, tx))
alice2blockchain.expectNoMessage(100 millis)
// Alice receives the preimage for the incoming HTLC.
@@ -1152,7 +1181,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// The commit transaction and main transactions confirm.
alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 3, closingStateAlice.commitTx)
- alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 5, closingTxsAlice.anchorTx)
+ alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 5, closingTxsAlice.anchorTx_opt.get)
alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 1, closingTxsAlice.mainTx_opt.get)
alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 2, closingTxsBob.mainTx_opt.get)
alice2blockchain.expectNoMessage(100 millis)
@@ -1359,6 +1388,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit
}
+ test("recv WatchFundingSpentTriggered (remote commit, zero-fee commitments with small balance)", Tag(ChannelStateTestsTags.ZeroFeeCommitments), Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
+ import f._
+
+ // Alice sends almost all of her balance to Bob.
+ val aliceInitialBalance = alice.commitments.availableBalanceForSend
+ addHtlc(aliceInitialBalance, alice, bob, alice2bob, bob2alice)
+ crossSign(alice, bob, alice2bob, bob2alice)
+
+ // The feerate is quite high for fast inclusion in the blockchain.
+ alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(20_000 sat)).copy(minimum = FeeratePerKw(253 sat), slow = FeeratePerKw(500 sat)))
+ // Bob force-closes while the HTLC is pending.
+ val bobCommitTx = bob.signCommitTx()
+ alice ! WatchFundingSpentTriggered(bobCommitTx)
+ // Alice claims the remote anchor, because her main output is too small to be used for CPFP.
+ // She has a pending outgoing HTLC, which means some funds are at risk.
+ val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
+ val htlcTimeoutTx = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]
+ alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
+ alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx.input.outPoint, mainTx.input, htlcTimeoutTx.input.outPoint))
+ alice2blockchain.expectNoMessage(100 millis)
+ }
+
test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)") { f =>
import f._
@@ -1433,13 +1485,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob publishes the latest commit tx.
val bobCommitTx = bob.signCommitTx()
- assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs
+ commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs
+ case ZeroFeeCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + one anchors + 3 HTLCs
+ }
val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3)
assert(closingState.htlcOutputs.size == 3)
assert(closingTxs.htlcTimeoutTxs.length == 3)
alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx)
- // for static_remote_key channels there is no claimMainOutputTx (bob's commit tx directly sends to our wallet)
closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx))
alice2relayer.expectNoMessage(100 millis)
alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0))
@@ -1467,6 +1521,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv WatchTxConfirmedTriggered (remote commit) followed by htlc settlement") { f =>
import f._
// Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed.
@@ -1498,7 +1556,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob's commitment confirms: the third htlc was not included in the commit tx published on-chain, so we can consider it failed.
alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx)
- alice ! WatchTxConfirmedTriggered(BlockHeight(0), 1, closingTxs.anchorTx)
+ closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 1, tx))
assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc3)
// Alice's main transaction confirms.
closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx))
@@ -1546,7 +1604,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Our main transaction should have a lower feerate.
// HTLC transactions are unchanged: the feerate will be based on their expiry.
closingTxs.mainTx_opt.foreach(tx => {
- val tx2 = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val tx2 = alice2blockchain.expectFinalTxPublished("remote-main")
assert(tx2.tx.txIn.head.outPoint == tx.txIn.head.outPoint)
assert(tx2.tx.txOut.head.amount > tx.txOut.head.amount)
})
@@ -1556,6 +1614,28 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectWatchOutputsSpent(Seq(closingTxs.mainTx_opt.get.txIn.head.outPoint, htlcTimeout.input.outPoint))
}
+ test("recv INPUT_RESTORED (remote commit, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ import f._
+
+ val bobCommitTx = bob.signCommitTx()
+ val (_, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain)
+ // Alice uses her main output to bump the commit fees instead of using a dedicated anchor transaction.
+ assert(closingTxs.anchorTx_opt.isEmpty)
+ // The remote commitment confirms independently (e.g. Bob spent the anchor output with his wallet inputs).
+ alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx)
+
+ // We simulate a node restart.
+ val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING]
+ alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing)
+ alice ! INPUT_RESTORED(beforeRestart)
+ alice2blockchain.expectMsgType[SetChannelId]
+ // We don't need to spend the anchor output anymore with our main transaction: it is a simple standalone transaction.
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
+ assert(mainTx.input == closingTxs.mainTx_opt.get.txIn.head.outPoint)
+ alice2blockchain.expectWatchOutputSpent(mainTx.input)
+ alice2blockchain.expectNoMessage(100 millis)
+ }
+
private def testNextRemoteCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): (Transaction, PublishedForceCloseTxs, Set[UpdateAddHtlc]) = {
import f._
@@ -1662,7 +1742,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob's commitment and Alice's main transaction confirm.
alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx)
- alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.anchorTx)
+ closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx))
closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx))
// Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output.
@@ -1706,7 +1786,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectMsgType[WatchFundingSpent]
// then we should re-publish unconfirmed transactions
val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]
- closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("remote-main-delayed"))
+ closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("remote-main"))
val htlcTimeoutTxs = closingTxs.htlcTxs.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx])
assert(htlcTimeoutTxs.map(_.input.outPoint).toSet == closingTxs.htlcTxs.map(_.txIn.head.outPoint).toSet)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
@@ -1761,7 +1841,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)
// alice is able to claim its main output
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice2blockchain.expectWatchOutputSpent(mainTx.input)
@@ -1778,7 +1858,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
// alice is able to claim its main output
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice2blockchain.expectWatchOutputSpent(mainTx.input)
@@ -1802,7 +1882,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSING)
// bob republishes his main transaction
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid)
alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx)
alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx)
@@ -1818,7 +1898,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob's first commit tx doesn't contain any htlc
val bobCommit1 = RevokedCommit(bob.signCommitTx(), Nil)
- assert(bobCommit1.commitTx.txOut.size == 4) // 2 main outputs + 2 anchors
+ bob.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 4) // 2 main outputs + 2 anchors
+ case ZeroFeeCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 3) // 2 main outputs + 1 anchor
+ }
// Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc
val (bobCommit2, htlcAlice1, htlcBob1) = {
@@ -1831,7 +1914,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
assert(alice.signCommitTx().txOut.size == bobCommit2.commitTx.txOut.size)
- assert(bobCommit2.commitTx.txOut.size == 6)
+ bob.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 6)
+ case ZeroFeeCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 5)
+ }
// Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs
val (bobCommit3, htlcAlice2, htlcBob2) = {
@@ -1844,7 +1930,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
assert(alice.signCommitTx().txOut.size == bobCommit3.commitTx.txOut.size)
- assert(bobCommit3.commitTx.txOut.size == 8)
+ bob.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 8)
+ case ZeroFeeCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 7)
+ }
// Bob's fourth commit tx doesn't contain any htlc
val bobCommit4 = {
@@ -1855,7 +1944,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
assert(alice.signCommitTx().txOut.size == bobCommit4.commitTx.txOut.size)
- assert(bobCommit4.commitTx.txOut.size == 4)
+ bob.commitments.latest.commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 4)
+ case ZeroFeeCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 3)
+ }
RevokedCloseFixture(Seq(bobCommit1, bobCommit2, bobCommit3, bobCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2))
}
@@ -1882,7 +1974,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(rvk.htlcDelayedOutputs.isEmpty)
// alice publishes the penalty txs
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty")
Transaction.correctlySpends(mainPenaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
@@ -1891,7 +1983,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet
val spentOutpoints = Seq(mainTx.input, mainPenaltyTx.input) ++ htlcPenaltyTxs.map(_.input)
- assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors
+ commitmentFormat match {
+ case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors
+ case ZeroFeeCommitmentFormat => assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim the anchor
+ }
// alice watches on-chain transactions
alice2blockchain.expectWatchTxConfirmed(bobRevokedTx.txid)
@@ -1907,7 +2002,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (bobRevokedTx, closingTxs) = setupFundingSpentRevokedTx(f, commitmentFormat)
val txPublished = txListener.expectMsgType[TransactionPublished]
assert(txPublished.tx == bobRevokedTx)
- assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the revoked commit
// once all txs are confirmed, alice can move to the closed state
alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, bobRevokedTx)
@@ -1936,6 +2030,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testFundingSpentRevokedTx(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv WatchFundingSpentTriggered (one revoked tx, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testFundingSpentRevokedTx(f, ZeroFeeCommitmentFormat)
+ }
+
test("recv WatchFundingSpentTriggered (multiple revoked tx)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f =>
import f._
val revokedCloseFixture = prepareRevokedClose(f)
@@ -1948,7 +2046,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx)
// alice publishes penalty txs
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
(mainTx.tx +: mainPenalty.tx +: htlcPenaltyTxs.map(_.tx)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
@@ -1992,7 +2090,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// the commit tx hasn't been confirmed yet, so we watch the funding output first
alice2blockchain.expectMsgType[WatchFundingSpent]
// then we should re-publish unconfirmed transactions
- alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ alice2blockchain.expectFinalTxPublished("remote-main")
assert(alice2blockchain.expectFinalTxPublished("main-penalty").input == closingTxs.mainPenaltyTx.txIn.head.outPoint)
val htlcPenaltyTxs = closingTxs.htlcPenaltyTxs.map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
assert(htlcPenaltyTxs.map(_.input).toSet == closingTxs.htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet)
@@ -2033,7 +2131,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(rvk.htlcDelayedOutputs.isEmpty)
// alice publishes the penalty txs and watches outputs
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid)
@@ -2121,7 +2219,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(rvk.htlcDelayedOutputs.isEmpty)
// alice publishes the penalty txs and watches outputs
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid)
@@ -2180,6 +2278,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)
}
+ test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx, zero-fee commitments)", Tag(ChannelStateTestsTags.ZeroFeeCommitments)) { f =>
+ testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeCommitmentFormat)
+ }
+
def testInputRestoredRevokedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = {
import f._
@@ -2193,7 +2295,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice.commitments.latest.commitmentFormat == commitmentFormat)
// Alice publishes the penalty txs and watches outputs.
- val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx = alice2blockchain.expectFinalTxPublished("remote-main")
val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty")
val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty"))
alice2blockchain.expectWatchTxConfirmed(commitTx.txid)
@@ -2257,7 +2359,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSING)
// We re-publish closing transactions with a higher feerate.
- val mainTx2 = alice2blockchain.expectFinalTxPublished("remote-main-delayed")
+ val mainTx2 = alice2blockchain.expectFinalTxPublished("remote-main")
assert(mainTx2.input == mainTx.input)
assert(mainTx2.tx.txOut.head.amount < mainTx.tx.txOut.head.amount)
Transaction.correctlySpends(mainTx2.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
index 761da2b..2b51f9b 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
@@ -34,6 +34,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment
iWhy this scored 34/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.