From de12938b4a7b01e0adc436656ff7862b2937ccc3 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 15 Jun 2022 14:59:57 +0200 Subject: [PATCH 1/4] Implement first steps of the dual funding flow We implement the first step of the dual funding protocol: exchanging `open_channel2` and `accept_channel2`. We currently stop after exchanging these two messages. Future commits will add the interactive-tx protocol used to build the funding transaction. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 8 +- .../fr/acinq/eclair/channel/ChannelData.scala | 106 ++++---- .../fr/acinq/eclair/channel/Helpers.scala | 109 ++++++-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 59 ++--- .../channel/fsm/ChannelOpenDualFunded.scala | 233 ++++++++++++++++++ .../channel/fsm/ChannelOpenSingleFunder.scala | 45 +++- .../main/scala/fr/acinq/eclair/io/Peer.scala | 132 ++++++---- .../fr/acinq/eclair/EclairImplSpec.scala | 4 +- .../scala/fr/acinq/eclair/TestConstants.scala | 3 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 5 +- .../ChannelStateTestsHelperMethods.scala | 19 +- .../a/WaitForAcceptChannelStateSpec.scala | 8 +- ...tForAcceptDualFundedChannelStateSpec.scala | 165 +++++++++++++ .../a/WaitForOpenChannelStateSpec.scala | 4 +- ...aitForOpenDualFundedChannelStateSpec.scala | 171 +++++++++++++ .../b/WaitForFundingCreatedStateSpec.scala | 4 +- .../b/WaitForFundingInternalStateSpec.scala | 4 +- .../b/WaitForFundingSignedStateSpec.scala | 4 +- .../c/WaitForChannelReadyStateSpec.scala | 6 +- .../c/WaitForFundingConfirmedStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 4 +- .../eclair/integration/IntegrationSpec.scala | 10 +- .../basic/fixtures/MinimalNodeFixture.scala | 5 +- .../interop/rustytests/RustyTestsSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 113 ++++++--- 25 files changed, 999 insertions(+), 230 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 570546560c..670c2b2fea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -186,11 +186,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { _ <- Future.successful(0) open = Peer.OpenChannel( remoteNodeId = nodeId, - fundingSatoshis = fundingAmount, - pushMsat = pushAmount_opt.getOrElse(0 msat), + fundingAmount = fundingAmount, channelType_opt = channelType_opt, - fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), + pushAmount_opt = pushAmount_opt, + fundingTxFeerate_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), timeout_opt = Some(openTimeout)) res <- (appKit.switchboard ? open).mapTo[ChannelOpenResponse] } yield res diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 0cf0bdba1f..98acf213b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -23,8 +23,8 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, Alias, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, RealShortChannelId, UInt64} import scodec.bits.ByteVector import java.util.UUID @@ -47,6 +47,8 @@ import java.util.UUID */ sealed trait ChannelState case object WAIT_FOR_INIT_INTERNAL extends ChannelState +// Single-funder channel opening: +case object WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL extends ChannelState case object WAIT_FOR_OPEN_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState @@ -54,6 +56,12 @@ case object WAIT_FOR_FUNDING_CREATED extends ChannelState case object WAIT_FOR_FUNDING_SIGNED extends ChannelState case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState case object WAIT_FOR_CHANNEL_READY extends ChannelState +// Dual-funded channel opening: +case object WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_DUAL_FUNDING_INTERNAL extends ChannelState +// Channel opened: case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState case object NEGOTIATING extends ChannelState @@ -75,25 +83,29 @@ case object ERR_INFORMATION_LEAK extends ChannelState 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ -case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, - fundingAmount: Satoshi, - pushAmount: MilliSatoshi, - commitTxFeerate: FeeratePerKw, - fundingTxFeerate: FeeratePerKw, - localParams: LocalParams, - remote: ActorRef, - remoteInit: Init, - channelFlags: ChannelFlags, - channelConfig: ChannelConfig, - channelType: SupportedChannelType) { +case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, + fundingAmount: Satoshi, + dualFunded: Boolean, + commitTxFeerate: FeeratePerKw, + fundingTxFeerate: FeeratePerKw, + pushAmount_opt: Option[MilliSatoshi], + localParams: LocalParams, + remote: ActorRef, + remoteInit: Init, + channelFlags: ChannelFlags, + channelConfig: ChannelConfig, + channelType: SupportedChannelType) { require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels") } -case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, - localParams: LocalParams, - remote: ActorRef, - remoteInit: Init, - channelConfig: ChannelConfig, - channelType: SupportedChannelType) +case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32, + fundingContribution_opt: Option[Satoshi], + dualFunded: Boolean, + localParams: LocalParams, + remote: ActorRef, + remoteInit: Init, + channelConfig: ChannelConfig, + channelType: SupportedChannelType) + case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init) @@ -363,6 +375,28 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti } } +sealed trait RealScidStatus { def toOption: Option[RealShortChannelId] } +object RealScidStatus { + /** The funding transaction has been confirmed but hasn't reached min_depth, we must be ready for a reorg. */ + case class Temporary(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** The funding transaction has been deeply confirmed. */ + case class Final(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** We don't know the status of the funding transaction. */ + case object Unknown extends RealScidStatus { override def toOption: Option[RealShortChannelId] = None } +} + +/** + * Short identifiers for the channel + * + * @param real the real scid, it may change if a reorg happens before the channel reaches 6 conf + * @param localAlias we must remember the alias that we sent to our peer because we use it to: + * - identify incoming [[ChannelUpdate]] at the connection level + * - route outgoing payments to that channel + * @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate + * routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]] + */ +case class ShortIds(real: RealScidStatus, localAlias: Alias, remoteAlias_opt: Option[Alias]) + sealed trait ChannelData extends PossiblyHarmful { def channelId: ByteVector32 } @@ -378,10 +412,10 @@ sealed trait PersistentChannelData extends ChannelData { def commitments: Commitments } -final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends TransientChannelData { +final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends TransientChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, @@ -430,29 +464,17 @@ final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, shortIds: ShortIds, lastSent: ChannelReady) extends PersistentChannelData -sealed trait RealScidStatus { def toOption: Option[RealShortChannelId] } -object RealScidStatus { - /** The funding transaction has been confirmed but hasn't reached min_depth, we must be ready for a reorg. */ - case class Temporary(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } - /** The funding transaction has been deeply confirmed. */ - case class Final(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } - /** We don't know the status of the funding transaction. */ - case object Unknown extends RealScidStatus { override def toOption: Option[RealShortChannelId] = None } +final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { + val channelId: ByteVector32 = init.temporaryChannelId +} +final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenDualFundedChannel) extends TransientChannelData { + val channelId: ByteVector32 = lastSent.temporaryChannelId } +final case class DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId: ByteVector32, + localParams: LocalParams, + remoteParams: RemoteParams, + channelFeatures: ChannelFeatures) extends TransientChannelData -/** - * Short identifiers for the channel - * - * @param real the real scid, it may change if a reorg happens before the channel reaches 6 conf - * @param localAlias we must remember the alias that we sent to our peer because we use it to: - * - identify incoming [[ChannelUpdate]] at the connection level - * - route outgoing payments to that channel - * @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate - * routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]] - */ -case class ShortIds(real: RealScidStatus, - localAlias: Alias, - remoteAlias_opt: Option[Alias]) final case class DATA_NORMAL(commitments: Commitments, shortIds: ShortIds, channelAnnouncement: Option[ChannelAnnouncement], 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 a6f0b3b2a7..e8dd5a309a 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 @@ -80,9 +80,7 @@ object Helpers { } } - /** - * Called by the fundee - */ + /** Called by the fundee of a singler-funded channel. */ def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. @@ -109,6 +107,7 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (open.dustLimitSatoshis > open.channelReserveSatoshis) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, open.channelReserveSatoshis)) + if (open.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUST_LIMIT)) // BOLT #2: The receiving node MUST fail the channel if both to_local and to_remote amounts for the initial commitment // transaction are less than or equal to channel_reserve_satoshis (see BOLT 3). @@ -120,10 +119,6 @@ object Helpers { // 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(remoteNodeId, channelType, open.fundingSatoshis, None) if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)) - // only enforce dust limit check on mainnet - if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { - if (open.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUST_LIMIT)) - } // we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment // now, but it will be done later when we receive `funding_created` @@ -135,31 +130,66 @@ object Helpers { extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } - /** - * Called by the funder - */ - def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { - accept.channelType_opt match { - case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => + /** Called by the non-initiator of a dual-funded channel. */ + def validateParamsNonInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, open: OpenDualFundedChannel, remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: + // MUST reject the channel. + if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) + + if (open.fundingAmount < nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel) || open.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), nodeParams.channelConf.maxFundingSatoshis)) + + // BOLT #2: Channel funding limits + if (open.fundingAmount >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Channel.MAX_FUNDING)) + + // BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large. + if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || 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)) + + // 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(remoteNodeId, channelType, open.fundingAmount, None) + if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate)) + + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) + } + + private def validateChannelType(channelId: ByteVector32, channelType: SupportedChannelType, channelFlags: ChannelFlags, openChannelType_opt: Option[ChannelType], acceptChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Option[ChannelException] = { + acceptChannelType_opt match { + case Some(theirChannelType) if acceptChannelType_opt != openChannelType_opt => // if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, theirChannelType)) + Some(InvalidChannelType(channelId, channelType, theirChannelType)) case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - return Left(MissingChannelType(open.temporaryChannelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel) => + Some(MissingChannelType(channelId)) + case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel))) - case _ => // we agree on channel type + Some(InvalidChannelType(channelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel))) + case _ => + // we agree on channel type + None } + } - if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) - // only enforce dust limit check on mainnet - if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { - if (accept.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUST_LIMIT)) + /** Called by the funder of a single-funder channel. */ + def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { + case Some(t) => return Left(t) + case None => // we agree on channel type } + if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) + 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)) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (accept.dustLimitSatoshis > accept.channelReserveSatoshis) return Left(DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, accept.channelReserveSatoshis)) @@ -183,6 +213,41 @@ object Helpers { extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } + /** Called by the initiator of a dual-funded channel. */ + def validateParamsInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { + case Some(t) => return Left(t) + case None => // we agree on channel type + } + + if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) + + 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)) + + // if minimum_depth is unreasonably large: + // MAY reject the channel. + if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) + + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) + } + + /** Compute the temporaryChannelId of a dual-funded channel. */ + def dualFundedTemporaryChannelId(nodeParams: NodeParams, localParams: LocalParams, channelConfig: ChannelConfig): ByteVector32 = { + val channelKeyPath = nodeParams.channelKeyManager.keyPath(localParams, channelConfig) + val revocationBasepoint = nodeParams.channelKeyManager.revocationPoint(channelKeyPath).publicKey + Crypto.sha256(ByteVector.fill(33)(0) ++ revocationBasepoint.value) + } + + /** Compute the channelId of a dual-funded channel. */ + def computeChannelId(open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): ByteVector32 = { + val bin = Seq(open.revocationBasepoint.value, accept.revocationBasepoint.value) + .sortWith(LexicographicalOrdering.isLessThan) + .reduce(_ ++ _) + Crypto.sha256(bin) + } + /** * We use the real scid if the channel has been announced, otherwise we use our local alias. */ 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 e2203932db..850fa59876 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 @@ -47,7 +47,6 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.ClosingTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ -import scodec.bits.ByteVector import scala.collection.immutable.Queue import scala.concurrent.ExecutionContext @@ -164,6 +163,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val extends FSM[ChannelState, ChannelData] with FSMDiagnosticActorLogging[ChannelState, ChannelData] with ChannelOpenSingleFunder + with ChannelOpenDualFunded with CommonHandlers with FundingHandlers with ErrorHandlers { @@ -208,43 +208,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, commitTxFeerate, Some(fundingTxFeerate))) - activeConnection = remote - txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty - val open = OpenChannel(nodeParams.chainHash, - temporaryChannelId = temporaryChannelId, - fundingSatoshis = fundingSatoshis, - pushMsat = pushMsat, - dustLimitSatoshis = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), - htlcMinimumMsat = localParams.htlcMinimum, - feeratePerKw = commitTxFeerate, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubKey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - channelFlags = channelFlags, - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(channelType) - )) - goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open - - case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isInitiator => - activeConnection = remote - txPublisher ! SetChannelId(remoteNodeId, inputFundee.temporaryChannelId) - goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee) + case Event(input: INPUT_INIT_CHANNEL_INITIATOR, Nothing) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, input.temporaryChannelId, input.commitTxFeerate, Some(input.fundingTxFeerate))) + activeConnection = input.remote + txPublisher ! SetChannelId(remoteNodeId, input.temporaryChannelId) + // We will process the input in the next state differently depending on whether we use dual-funding or not. + self ! input + if (input.dualFunded) { + goto(WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL) + } else { + goto(WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL) + } + + case Event(input: INPUT_INIT_CHANNEL_NON_INITIATOR, Nothing) if !input.localParams.isInitiator => + activeConnection = input.remote + txPublisher ! SetChannelId(remoteNodeId, input.temporaryChannelId) + if (input.dualFunded) { + goto(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) using DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(input) + } else { + goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(input) + } case Event(INPUT_RESTORED(data), _) => log.debug("restoring channel") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala new file mode 100644 index 0000000000..cd719ce43f --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -0,0 +1,233 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.eclair.Features +import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, AcceptDualFundedChannelTlv, ChannelTlv, Error, OpenDualFundedChannel, OpenDualFundedChannelTlv, TlvStream} + +/** + * Created by t-bast on 19/04/2022. + */ + +trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { + + this: Channel => + + /* + INITIATOR NON_INITIATOR + | | + | open_channel2 | WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL + |-------------------------------->| + WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL | | + | accept_channel2 | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_COMPLETE | | WAIT_FOR_DUAL_FUNDING_COMPLETE + | | + | . | + | . | + | . | + | tx_complete | + |-------------------------------->| + | tx_complete | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_SIGNED | | WAIT_FOR_DUAL_FUNDING_SIGNED + | commitment_signed | + |-------------------------------->| + | commitment_signed | + |<--------------------------------| + | tx_signatures | + |<--------------------------------| + | tx_signatures | + |-------------------------------->| + WAIT_FOR_DUAL_FUNDING_LOCKED | | WAIT_FOR_DUAL_FUNDING_LOCKED + | funding_locked funding_locked | + |---------------- ---------------| + | \/ | + | /\ | + |<--------------- -------------->| + NORMAL | | NORMAL + */ + + when(WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL)(handleExceptions { + case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => + val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) + val tlvs: TlvStream[OpenDualFundedChannelTlv] = if (Features.canUseFeature(input.localParams.initFeatures, input.remoteInit.features, Features.UpfrontShutdownScript)) { + TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(input.localParams.defaultFinalScriptPubKey), ChannelTlv.ChannelTypeTlv(input.channelType)) + } else { + TlvStream(ChannelTlv.ChannelTypeTlv(input.channelType)) + } + val open = OpenDualFundedChannel( + chainHash = nodeParams.chainHash, + temporaryChannelId = input.temporaryChannelId, + fundingFeerate = input.fundingTxFeerate, + commitmentFeerate = input.commitTxFeerate, + fundingAmount = input.fundingAmount, + dustLimit = input.localParams.dustLimit, + maxHtlcValueInFlightMsat = input.localParams.maxHtlcValueInFlightMsat, + htlcMinimum = input.localParams.htlcMinimum, + toSelfDelay = input.localParams.toSelfDelay, + maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, + lockTime = nodeParams.currentBlockHeight.toLong, + fundingPubkey = fundingPubKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + channelFlags = input.channelFlags, + tlvStream = tlvs) + goto(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) using DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(input, open) sending open + }) + + when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions { + case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => + import d.init.{localParams, remoteInit} + Helpers.validateParamsNonInitiator(nodeParams, d.init.channelType, open, remoteNodeId, localParams.initFeatures, remoteInit.features) match { + case Left(t) => handleLocalError(t, d, Some(open)) + case Right((channelFeatures, remoteShutdownScript)) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate))) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig) + val totalFundingAmount = open.fundingAmount + d.init.fundingContribution_opt.getOrElse(0 sat) + val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, totalFundingAmount) + val tlvs: TlvStream[AcceptDualFundedChannelTlv] = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) { + TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(localParams.defaultFinalScriptPubKey), ChannelTlv.ChannelTypeTlv(d.init.channelType)) + } else { + TlvStream(ChannelTlv.ChannelTypeTlv(d.init.channelType)) + } + val accept = AcceptDualFundedChannel( + temporaryChannelId = open.temporaryChannelId, + fundingAmount = d.init.fundingContribution_opt.getOrElse(0 sat), + dustLimit = localParams.dustLimit, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + htlcMinimum = localParams.htlcMinimum, + minimumDepth = minimumDepth.getOrElse(0), + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubkey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + tlvStream = tlvs) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = open.dustLimit, + maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity + htlcMinimum = open.htlcMinimum, + toSelfDelay = open.toSelfDelay, + maxAcceptedHtlcs = open.maxAcceptedHtlcs, + fundingPubKey = open.fundingPubkey, + revocationBasepoint = open.revocationBasepoint, + paymentBasepoint = open.paymentBasepoint, + delayedPaymentBasepoint = open.delayedPaymentBasepoint, + htlcBasepoint = open.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + val channelId = Helpers.computeChannelId(open, accept) + peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) sending accept + } + + case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL)(handleExceptions { + case Event(accept: AcceptDualFundedChannel, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + import d.init.{localParams, remoteInit} + Helpers.validateParamsInitiator(nodeParams, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match { + case Left(t) => + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(t, d, Some(accept)) + case Right((channelFeatures, remoteShutdownScript)) => + val channelId = Helpers.computeChannelId(d.lastSent, accept) + peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = accept.dustLimit, + maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity + htlcMinimum = accept.htlcMinimum, + toSelfDelay = accept.toSelfDelay, + maxAcceptedHtlcs = accept.maxAcceptedHtlcs, + fundingPubKey = accept.fundingPubkey, + revocationBasepoint = accept.revocationBasepoint, + paymentBasepoint = accept.paymentBasepoint, + delayedPaymentBasepoint = accept.delayedPaymentBasepoint, + htlcBasepoint = accept.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_DUAL_FUNDING_INTERNAL)(handleExceptions { + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 7e53291e79..e110e7cfaa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.TxOwner import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{Features, RealShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import fr.acinq.eclair.{Features, MilliSatoshiLong, RealShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -72,8 +72,45 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { NORMAL| |NORMAL */ + when(WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL)(handleExceptions { + case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => + val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (Features.canUseFeature(input.localParams.initFeatures, input.remoteInit.features, Features.UpfrontShutdownScript)) { + input.localParams.defaultFinalScriptPubKey + } else { + ByteVector.empty + } + val open = OpenChannel( + chainHash = nodeParams.chainHash, + temporaryChannelId = input.temporaryChannelId, + fundingSatoshis = input.fundingAmount, + pushMsat = input.pushAmount_opt.getOrElse(0 msat), + dustLimitSatoshis = input.localParams.dustLimit, + maxHtlcValueInFlightMsat = input.localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = input.localParams.requestedChannelReserve_opt.getOrElse(0 sat), + htlcMinimumMsat = input.localParams.htlcMinimum, + feeratePerKw = input.commitTxFeerate, + toSelfDelay = input.localParams.toSelfDelay, + maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + channelFlags = input.channelFlags, + tlvStream = TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(input.channelType) + )) + goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open + }) + when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { - case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => + case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_CHANNEL_NON_INITIATOR(_, _, _, localParams, _, remoteInit, channelConfig, channelType))) => Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => @@ -130,7 +167,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, fundingSatoshis, _, commitTxFeerate, fundingTxFeerate, pushMsat_opt, localParams, _, remoteInit, _, channelConfig, channelType), open)) => Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) @@ -156,7 +193,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat_opt.getOrElse(0 msat), commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => 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 4ad26188b5..892923dcb6 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 @@ -141,24 +141,24 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() case Event(c: Peer.OpenChannel, d: ConnectedData) => - if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.localFeatures.hasFeature(Wumbo)) { - sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)")) + if (c.fundingAmount >= Channel.MAX_FUNDING && !d.localFeatures.hasFeature(Wumbo)) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)")) stay() - } else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.remoteFeatures.hasFeature(Wumbo)) { - sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo")) + } else if (c.fundingAmount >= Channel.MAX_FUNDING && !d.remoteFeatures.hasFeature(Wumbo)) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} is too big, the remote peer doesn't support wumbo")) stay() - } else if (c.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) { - sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) + } else if (c.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay() } else { // If a channel type was provided, we directly use it instead of computing it based on local and remote features. - val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) - val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = channelFlags.announceChannel)) + val channelFlags = c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags) + val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, channelFlags.announceChannel)) // NB: we need to capture parameters in a val to use them in andThen val selfRef = self val origin = sender() implicit val ec: ExecutionContext = ExecutionContext.Implicits.global - createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis).andThen { + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingAmount).andThen { case Success(localParams) => selfRef ! SpawnChannelInitiator(c, ChannelConfig.standard, channelType, localParams, origin) case Failure(t) => origin ! Status.Failure(new RuntimeException("channel creation failed", t)) } @@ -168,54 +168,83 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA case Event(SpawnChannelInitiator(c, channelConfig, channelType, localParams, origin), d: ConnectedData) => val channel = spawnChannel(Some(origin)) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) - val temporaryChannelId = randomBytes32() - val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) - val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) - log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) + val dualFunded = Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) + val temporaryChannelId = if (dualFunded) { + Helpers.dualFundedTemporaryChannelId(nodeParams, localParams, channelConfig) + } else { + randomBytes32() + } + val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingAmount, None) + log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=$fundingTxFeerate temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.pushAmount_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) - case Event(msg: protocol.OpenChannel, d: ConnectedData) => - d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { + case Event(open: protocol.OpenChannel, d: ConnectedData) => + d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { case None => - val chosenChannelType: Either[ChannelException, SupportedChannelType] = msg.channelType_opt match { - // remote explicitly specifies a channel type: we check whether we want to allow it - case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { - case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel), remoteChannelType)) - } - // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) - // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel)) + validateRemoteChannelType(open.temporaryChannelId, open.channelFlags, open.channelType_opt, d.localFeatures, d.remoteFeatures) match { + case Right(channelType) => + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, open.fundingSatoshis).andThen { + case Success(localParams) => selfRef ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, channelType, localParams) + case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "channel creation failed"), d.peerConnection) + } + stay() + case Left(ex) => + log.warning("ignoring open_channel2: {}", ex.getMessage) + val err = Error(open.temporaryChannelId, ex.getMessage) + self ! Peer.OutgoingMessage(err, d.peerConnection) + stay() } - chosenChannelType match { + case Some(_) => + log.warning("ignoring open_channel with duplicate temporaryChannelId={}", open.temporaryChannelId) + stay() + } + + case Event(open: protocol.OpenDualFundedChannel, d: ConnectedData) => + d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { + case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) => + validateRemoteChannelType(open.temporaryChannelId, open.channelFlags, open.channelType_opt, d.localFeatures, d.remoteFeatures) match { case Right(channelType) => // NB: we need to capture parameters in a val to use them in andThen val selfRef = self implicit val ec: ExecutionContext = ExecutionContext.Implicits.global - createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, msg.fundingSatoshis).andThen { - case Success(localParams) => selfRef ! SpawnChannelNonInitiator(msg, ChannelConfig.standard, channelType, localParams) - case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(msg.temporaryChannelId, "channel creation failed"), d.peerConnection) + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, open.fundingAmount).andThen { + case Success(localParams) => selfRef ! SpawnChannelNonInitiator(Right(open), ChannelConfig.standard, channelType, localParams) + case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "channel creation failed"), d.peerConnection) } stay() case Left(ex) => - log.warning(s"ignoring open_channel: ${ex.getMessage}") - val err = Error(msg.temporaryChannelId, ex.getMessage) + log.warning("ignoring open_channel2: {}", ex.getMessage) + val err = Error(open.temporaryChannelId, ex.getMessage) self ! Peer.OutgoingMessage(err, d.peerConnection) stay() } + case None => + log.info("rejecting open_channel2: dual funding is not supported") + self ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "dual funding is not supported"), d.peerConnection) + stay() case Some(_) => - log.warning(s"ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}") + log.warning("ignoring open_channel2 with duplicate temporaryChannelId={}", open.temporaryChannelId) stay() } case Event(SpawnChannelNonInitiator(open, channelConfig, channelType, localParams), d: ConnectedData) => val channel = spawnChannel(None) - val temporaryChannelId = open.temporaryChannelId + val temporaryChannelId = open.fold(_.temporaryChannelId, _.temporaryChannelId) log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) - channel ! open + open match { + case Left(open) => + channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, None, dualFunded = false, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! open + case Right(open) => + // NB: we don't add a contribution to the funding amount. + channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, None, dualFunded = true, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! open + } stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: HasChannelId, d: ConnectedData) => @@ -402,6 +431,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA self ! Peer.OutgoingMessage(msg, peerConnection) } + def validateRemoteChannelType(temporaryChannelId: ByteVector32, channelFlags: ChannelFlags, remoteChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, SupportedChannelType] = { + remoteChannelType_opt match { + // remote explicitly specifies a channel type: we check whether we want to allow it + case Some(remoteChannelType) => ChannelTypes.areCompatible(localFeatures, remoteChannelType) match { + case Some(acceptedChannelType) => Right(acceptedChannelType) + case None => Left(InvalidChannelType(temporaryChannelId, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel), remoteChannelType)) + } + // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` + case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => Left(MissingChannelType(temporaryChannelId)) + // remote doesn't specify a channel type: we use spec-defined defaults + case None => Right(ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel)) + } + } + def stopPeer(): State = { log.info("removing peer from db") nodeParams.db.peers.removePeer(remoteNodeId) @@ -487,16 +530,19 @@ object Peer { } case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful - case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { - require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") - require(pushMsat <= fundingSatoshis, "pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis >= 0.sat, "fundingSatoshis must be positive") - require(pushMsat >= 0.msat, "pushMsat must be positive") - fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) + + case class OpenChannel(remoteNodeId: PublicKey, fundingAmount: Satoshi, channelType_opt: Option[SupportedChannelType], pushAmount_opt: Option[MilliSatoshi], fundingTxFeerate_opt: Option[FeeratePerKw], channelFlags_opt: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { + require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags_opt.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") + require(fundingAmount > 0.sat, s"funding amount must be positive") + pushAmount_opt.foreach(pushAmount => { + require(pushAmount >= 0.msat, s"pushAmount must be positive") + require(pushAmount <= fundingAmount, s"pushAmount must be less than or equal to funding amount") + }) + fundingTxFeerate_opt.foreach(feerate => require(feerate >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feerate is below minimum ${FeeratePerKw.MinimumFeeratePerKw}")) } private case class SpawnChannelInitiator(cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams, origin: ActorRef) - private case class SpawnChannelNonInitiator(msg: protocol.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams) + private case class SpawnChannelNonInitiator(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams) case class GetPeerInfo(replyTo: Option[typed.ActorRef[PeerInfoResponse]]) sealed trait PeerInfoResponse { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index b835882035..dd34358c16 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -98,12 +98,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // standard conversion eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt.contains(FeeratePerKw(1250 sat))) + assert(open.fundingTxFeerate_opt.contains(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) + assert(open1.fundingTxFeerate_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) assert(open1.channelType_opt.contains(ChannelTypes.StaticRemoteKey)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index e7366e416e..1b027c2137 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -44,7 +44,8 @@ import scala.concurrent.duration._ object TestConstants { val defaultBlockHeight = 400000 - val fundingSatoshis: Satoshi = 1000000L sat + val fundingSatoshis: Satoshi = 1000000 sat + val nonInitiatorFundingSatoshis: Satoshi = 500000 sat val pushMsat: MilliSatoshi = 200000000L msat val feeratePerKw: FeeratePerKw = FeeratePerKw(10000 sat) val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2500 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 987b686a7c..e0495ac19c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -42,7 +42,6 @@ import org.scalatest.{Outcome, Tag} import java.util.UUID import java.util.concurrent.CountDownLatch -import scala.collection.immutable.Nil import scala.concurrent.duration._ import scala.util.Random @@ -80,9 +79,9 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe aliceRegister ! alice bobRegister ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] pipe ! (alice, bob) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] 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 6a195691b0..cf03f1fe17 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 @@ -49,6 +49,8 @@ import scala.concurrent.duration._ object ChannelStateTestsTags { /** If set, channels will use option_support_large_channel. */ val Wumbo = "wumbo" + /** If set, channels will use option_dual_fund. */ + val DualFunding = "dual_funding" /** If set, channels will use option_static_remotekey. */ val StaticRemoteKey = "static_remotekey" /** If set, channels will use option_anchor_outputs. */ @@ -61,6 +63,8 @@ object ChannelStateTestsTags { val ChannelsPublic = "channels_public" /** If set, no amount will be pushed when opening a channel (by default we push a small amount). */ val NoPushMsat = "no_push_msat" + /** If set, the non-initiator of a dual-funded channel will contribute some funds. */ + val DualFundingContribution = "dual_funding_contribution" /** If set, max-htlc-value-in-flight will be set to the highest possible value for Alice and Bob. */ val NoMaxHtlcValueInFlight = "no_max_htlc_value_in_flight" /** If set, max-htlc-value-in-flight will be set to a low value for Alice. */ @@ -168,6 +172,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) @@ -179,6 +184,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .initFeatures() val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) @@ -213,20 +219,19 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { - (TestConstants.fundingSatoshis, 0.msat) - } else { - (TestConstants.fundingSatoshis, TestConstants.pushMsat) - } + val fundingAmount = TestConstants.fundingSatoshis + val pushMsat = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) 0 msat else TestConstants.pushMsat + val nonInitiatorFundingAmount = if (tags.contains(ChannelStateTestsTags.DualFundingContribution)) Some(TestConstants.nonInitiatorFundingSatoshis) else None + val dualFunded = tags.contains(ChannelStateTestsTags.DualFunding) val eventListener = TestProbe() systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFundingAmount, dualFunded, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 aaac7e188a..7203476f4f 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 @@ -64,8 +64,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) @@ -167,8 +167,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Bob advertises support for anchor outputs, but Alice doesn't. val aliceParams = Alice.channelParams val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob, open) 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 new file mode 100644 index 0000000000..97ef29e99c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala @@ -0,0 +1,165 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.a + +import akka.actor.Status +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel} +import fr.acinq.eclair.{Features, TestConstants, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], open: OpenDualFundedChannel, aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init(wallet = new NoOpOnChainWallet()) + import setup._ + + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.DualFundingContribution)) Some(TestConstants.nonInitiatorFundingSatoshis) else None + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob, open) + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, open, aliceOrigin, alice2bob, bob2alice))) + } + } + + test("recv AcceptDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + assert(accept.upfrontShutdownScript_opt.isEmpty) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(accept.fundingAmount == 0.sat) + + val listener = TestProbe() + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[ChannelIdAssigned]) + bob2alice.forward(alice, accept) + assert(listener.expectMsgType[ChannelIdAssigned].channelId == Helpers.computeChannelId(open, accept)) + + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + val channelFeatures = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].channelFeatures + assert(channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + assert(channelFeatures.hasFeature(Features.DualFunding)) + aliceOrigin.expectNoMessage() + } + + test("recv AcceptDualFundedChannel (with non-initiator contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.DualFundingContribution), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + assert(accept.upfrontShutdownScript_opt.isEmpty) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(accept.fundingAmount == TestConstants.nonInitiatorFundingSatoshis) + bob2alice.forward(alice, accept) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + } + + test("recv AcceptDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 + alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = alice2bob.expectMsgType[Error] + assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (dust limit too low)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val lowDustLimit = Channel.MIN_DUST_LIMIT - 1.sat + alice ! accept.copy(dustLimit = lowDustLimit) + val error = alice2bob.expectMsgType[Error] + assert(error == Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimit, Channel.MIN_DUST_LIMIT).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val highDustLimit = Alice.nodeParams.channelConf.maxRemoteDustLimit + 1.sat + alice ! accept.copy(dustLimit = highDustLimit) + val error = alice2bob.expectMsgType[Error] + assert(error == Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimit, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 + alice ! accept.copy(toSelfDelay = delayTooHigh) + val error = alice2bob.expectMsgType[Error] + assert(error == Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + alice ! Error(ByteVector32.Zeroes, "dual funding not supported") + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + alice ! TickChannelOpenTimeout + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + +} 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 f9692f40f2..8c81919dd5 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 @@ -57,8 +57,8 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) } 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 new file mode 100644 index 0000000000..79898cc8f6 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala @@ -0,0 +1,171 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.a + +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} +import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel} +import fr.acinq.eclair.{Features, TestConstants, TestKitBaseClass, randomBytes32} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, aliceListener: TestProbe, bobListener: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + + val aliceListener = TestProbe() + alice.underlyingActor.context.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelCreated]) + alice.underlyingActor.context.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelIdAssigned]) + val bobListener = TestProbe() + bob.underlyingActor.context.system.eventStream.subscribe(bobListener.ref, classOf[ChannelCreated]) + bob.underlyingActor.context.system.eventStream.subscribe(bobListener.ref, classOf[ChannelIdAssigned]) + + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + awaitCond(bob.stateName == WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, aliceListener, bobListener))) + } + } + + test("recv OpenDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + assert(open.upfrontShutdownScript_opt.isEmpty) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(open.fundingFeerate == TestConstants.feeratePerKw) + assert(open.commitmentFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(open.lockTime == TestConstants.defaultBlockHeight) + + val initiatorEvent = aliceListener.expectMsgType[ChannelCreated] + assert(initiatorEvent.isInitiator) + assert(initiatorEvent.temporaryChannelId == ByteVector32.Zeroes) + + alice2bob.forward(bob) + + val nonInitiatorEvent = bobListener.expectMsgType[ChannelCreated] + assert(!nonInitiatorEvent.isInitiator) + assert(nonInitiatorEvent.temporaryChannelId == ByteVector32.Zeroes) + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val channelIdAssigned = bobListener.expectMsgType[ChannelIdAssigned] + assert(channelIdAssigned.temporaryChannelId == ByteVector32.Zeroes) + assert(channelIdAssigned.channelId == Helpers.computeChannelId(open, accept)) + + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + val channelFeatures = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].channelFeatures + assert(channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + assert(channelFeatures.hasFeature(Features.DualFunding)) + } + + test("recv OpenDualFundedChannel (invalid chain)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val chain = randomBytes32() + bob ! open.copy(chainHash = chain) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, chain).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (funding too low)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + bob ! open.copy(fundingAmount = 100 sat) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, 100 sat, Bob.nodeParams.channelConf.minFundingSatoshis(false), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 + bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 + bob ! open.copy(toSelfDelay = delayTooHigh) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val dustLimitTooHigh = Bob.nodeParams.channelConf.maxRemoteDustLimit + 1.sat + bob ! open.copy(dustLimit = dustLimitTooHigh) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (dust limit too small)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val dustLimitTooSmall = Channel.MIN_DUST_LIMIT - 1.sat + bob ! open.copy(dustLimit = dustLimitTooSmall) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, DustLimitTooSmall(open.temporaryChannelId, dustLimitTooSmall, Channel.MIN_DUST_LIMIT).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + bob ! Error(ByteVector32.Zeroes, "dual funding not supported") + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val sender = TestProbe() + val cmd = CMD_CLOSE(sender.ref, None, None) + bob ! cmd + sender.expectMsg(RES_SUCCESS(cmd, ByteVector32.Zeroes)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == CLOSED) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index ff298ee255..66f8713012 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -65,9 +65,9 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 23419edfda..0fd48e21c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -48,8 +48,8 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index ddccfb0499..480fdf7513 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -64,9 +64,9 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index e08f1c3879..ce17487f0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -49,15 +49,15 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) None else Some(TestConstants.pushMsat) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, pushMsat, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index f2feb663ce..cbb5fac754 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -57,9 +57,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF within(30 seconds) { val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 4ad92de782..711f73c2b6 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 @@ -70,9 +70,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 1e383811e0..b05f48ec21 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -164,16 +164,16 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](10 seconds) } - def connect(node1: Kit, node2: Kit, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi): ChannelOpenResponse.ChannelOpened = { + def connect(node1: Kit, node2: Kit, fundingAmount: Satoshi, pushMsat: MilliSatoshi): ChannelOpenResponse.ChannelOpened = { val sender = TestProbe() connect(node1, node2) sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, - fundingSatoshis = fundingSatoshis, - pushMsat = pushMsat, + fundingAmount = fundingAmount, channelType_opt = None, - fundingTxFeeratePerKw_opt = None, - channelFlags = None, + pushAmount_opt = Some(pushMsat), + fundingTxFeerate_opt = None, + channelFlags_opt = None, timeout_opt = None)) sender.expectMsgType[ChannelOpenResponse.ChannelOpened](10 seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 647155a2b8..4025b3f2fc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.payment.relay.{ChannelRelayer, Relayer} import fr.acinq.eclair.payment.send.PaymentInitiator import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress -import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} import org.scalatest.concurrent.{Eventually, IntegrationPatience} import org.scalatest.{Assertions, EitherValues} @@ -35,7 +35,6 @@ import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration.DurationInt import scala.util.{Random, Try} - /** * A minimal node setup, with real actors. * @@ -161,7 +160,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): ChannelOpened = { val sender = TestProbe("sender") - sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0 msat, channelType_opt, None, None, None)) + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, channelType_opt, None, None, None, None)) sender.expectMsgType[ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index fa6b642e34..652e038848 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -75,9 +75,9 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Some(1000000000 msat), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] pipe ! (alice, bob) within(30 seconds) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index de1b274936..99ec42ba8c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -70,10 +70,11 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.ChannelType))(Features(ChannelType -> Optional)) - .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(StaticRemoteKey -> Optional)) - .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) - .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) - .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.StaticRemoteKey))(Features(StaticRemoteKey -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Features(Wumbo -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.AnchorOutputs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.DualFunding))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)) .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -275,7 +276,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage() peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId == open.temporaryChannelId) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].temporaryChannelId == open.temporaryChannelId) channel.expectMsg(open) // open_channel messages with the same temporary channel id should simply be ignored @@ -294,12 +295,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)") } - test("don't spawn a wumbo channel if remote doesn't support wumbo", Tag("wumbo")) { f => + test("don't spawn a wumbo channel if remote doesn't support wumbo", Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val probe = TestProbe() @@ -308,12 +309,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) // Bob doesn't support wumbo, Alice does assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, the remote peer doesn't support wumbo") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big, the remote peer doesn't support wumbo") } - test("don't spawn a channel if fundingSatoshis is greater than maxFundingSatoshis", Tag("high-max-funding-satoshis"), Tag("wumbo")) { f => + test("don't spawn a channel if fundingSatoshis is greater than maxFundingSatoshis", Tag("high-max-funding-satoshis"), Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val probe = TestProbe() @@ -322,9 +323,9 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(Wumbo -> Optional))) // Bob supports wumbo assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)") } test("don't spawn a channel if we don't support their channel type") { f => @@ -370,7 +371,40 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peerConnection.expectMsg(Error(open.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) } - test("use their channel type when spawning a channel", Tag("static_remotekey")) { f => + test("don't spawn a dual funded channel if not supported") { f => + import f._ + + connect(remoteNodeId, peer, peerConnection, switchboard) + val open = createOpenDualFundedChannelMessage() + peerConnection.send(peer, open) + peerConnection.expectMsg(Error(open.temporaryChannelId, "dual funding is not supported")) + } + + test("use dual-funding when available", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + // Both peers support option_dual_fund, so it is automatically used. + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) + assert(peer.stateData.channels.isEmpty) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded) + } + + test("accept dual-funded channels when available", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + // Both peers support option_dual_fund, so it is automatically used. + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) + assert(peer.stateData.channels.isEmpty) + val open = createOpenDualFundedChannelMessage() + peerConnection.send(peer, open) + awaitCond(peer.stateData.channels.nonEmpty) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].dualFunded) + channel.expectMsg(open) + } + + test("use their channel type when spawning a channel", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ // We both support option_static_remotekey but they want to open a standard channel. @@ -379,30 +413,32 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))) peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType == ChannelTypes.Standard) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] + assert(init.channelType == ChannelTypes.Standard) + assert(!init.dualFunded) channel.expectMsg(open) } - test("use requested channel type when spawning a channel", Tag("static_remotekey")) { f => + test("use requested channel type when spawning a channel", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.StaticRemoteKey) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.StaticRemoteKey) // We can create channels that don't use the features we have enabled. - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.Standard), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.Standard) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.Standard), None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.Standard) // We can create channels that use features that we haven't enabled. - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.AnchorOutputs), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.AnchorOutputs) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs), None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputs) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val probe = TestProbe() @@ -412,15 +448,16 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // We ensure the current network feerate is higher than the default anchor output feerate. val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputs) + assert(!init.dualFunded) assert(init.fundingAmount == 15000.sat) assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val probe = TestProbe() @@ -430,22 +467,24 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // We ensure the current network feerate is higher than the default anchor output feerate. val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + assert(!init.dualFunded) assert(init.fundingAmount == 15000.sat) assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } - test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => + test("use correct final script if option_static_remotekey is negotiated", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.StaticRemoteKey) + assert(!init.dualFunded) assert(init.localParams.walletStaticPaymentBasepoint.isDefined) assert(init.localParams.defaultFinalScriptPubKey == Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) } @@ -454,7 +493,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle import f._ intercept[IllegalArgumentException] { - Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, Some(ChannelFlags(announceChannel = true)), None) + Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, Some(ChannelFlags(announceChannel = true)), None) } } @@ -470,10 +509,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } val peer = TestFSMRef(new Peer(TestConstants.Alice.nodeParams, remoteNodeId, new DummyOnChainWallet(), channelFactory, switchboard.ref)) connect(remoteNodeId, peer, peerConnection, switchboard) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.fundingAmount == 15000.sat) - assert(init.pushAmount == 100.msat) + assert(init.pushAmount_opt.contains(100.msat)) } test("handle final channelId assigned in state DISCONNECTED") { f => @@ -545,4 +584,8 @@ object PeerSpec { protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private, openTlv) } + def createOpenDualFundedChannelMessage(): protocol.OpenDualFundedChannel = { + protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 25000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private) + } + } \ No newline at end of file From 88260420dab107cda02d1569b9712f72f16d1e64 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 9 Aug 2022 15:31:13 +0200 Subject: [PATCH 2/4] Rename WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL --- .../src/main/scala/fr/acinq/eclair/channel/ChannelData.scala | 2 +- .../src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 98acf213b6..a87d6842a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -48,7 +48,7 @@ import java.util.UUID sealed trait ChannelState case object WAIT_FOR_INIT_INTERNAL extends ChannelState // Single-funder channel opening: -case object WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL extends ChannelState +case object WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL extends ChannelState case object WAIT_FOR_OPEN_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState 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 850fa59876..f66f9ed9a7 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 @@ -217,7 +217,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (input.dualFunded) { goto(WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL) } else { - goto(WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL) + goto(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL) } case Event(input: INPUT_INIT_CHANNEL_NON_INITIATOR, Nothing) if !input.localParams.isInitiator => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index e110e7cfaa..64d0aa51a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -72,7 +72,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { NORMAL| |NORMAL */ - when(WAIT_FOR_INIT_SINGLE_FUNDER_CHANNEL)(handleExceptions { + when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) From 31c3ee911cca907b62b82f5bc8ecedcaa1ad0563 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 9 Aug 2022 15:58:08 +0200 Subject: [PATCH 3/4] Refactor Peer handling of remote channel open --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) 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 892923dcb6..d5538541d4 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 @@ -160,7 +160,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA implicit val ec: ExecutionContext = ExecutionContext.Implicits.global createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingAmount).andThen { case Success(localParams) => selfRef ! SpawnChannelInitiator(c, ChannelConfig.standard, channelType, localParams, origin) - case Failure(t) => origin ! Status.Failure(new RuntimeException("channel creation failed", t)) + case Failure(t) => origin.tell(Status.Failure(new RuntimeException("channel creation failed", t)), selfRef) } stay() } @@ -183,22 +183,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA case Event(open: protocol.OpenChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { case None => - validateRemoteChannelType(open.temporaryChannelId, open.channelFlags, open.channelType_opt, d.localFeatures, d.remoteFeatures) match { - case Right(channelType) => - // NB: we need to capture parameters in a val to use them in andThen - val selfRef = self - implicit val ec: ExecutionContext = ExecutionContext.Implicits.global - createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, open.fundingSatoshis).andThen { - case Success(localParams) => selfRef ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, channelType, localParams) - case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "channel creation failed"), d.peerConnection) - } - stay() - case Left(ex) => - log.warning("ignoring open_channel2: {}", ex.getMessage) - val err = Error(open.temporaryChannelId, ex.getMessage) - self ! Peer.OutgoingMessage(err, d.peerConnection) - stay() - } + handleOpenChannel(Left(open), open.temporaryChannelId, open.fundingSatoshis, open.channelFlags, open.channelType_opt, d) + stay() case Some(_) => log.warning("ignoring open_channel with duplicate temporaryChannelId={}", open.temporaryChannelId) stay() @@ -207,22 +193,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA case Event(open: protocol.OpenDualFundedChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) => - validateRemoteChannelType(open.temporaryChannelId, open.channelFlags, open.channelType_opt, d.localFeatures, d.remoteFeatures) match { - case Right(channelType) => - // NB: we need to capture parameters in a val to use them in andThen - val selfRef = self - implicit val ec: ExecutionContext = ExecutionContext.Implicits.global - createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, open.fundingAmount).andThen { - case Success(localParams) => selfRef ! SpawnChannelNonInitiator(Right(open), ChannelConfig.standard, channelType, localParams) - case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "channel creation failed"), d.peerConnection) - } - stay() - case Left(ex) => - log.warning("ignoring open_channel2: {}", ex.getMessage) - val err = Error(open.temporaryChannelId, ex.getMessage) - self ! Peer.OutgoingMessage(err, d.peerConnection) - stay() - } + handleOpenChannel(Right(open), open.temporaryChannelId, open.fundingAmount, open.channelFlags, open.channelType_opt, d) + stay() case None => log.info("rejecting open_channel2: dual funding is not supported") self ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "dual funding is not supported"), d.peerConnection) @@ -431,6 +403,23 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA self ! Peer.OutgoingMessage(msg, peerConnection) } + def handleOpenChannel(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], temporaryChannelId: ByteVector32, fundingAmount: Satoshi, channelFlags: ChannelFlags, channelType_opt: Option[ChannelType], d: ConnectedData): Unit = { + validateRemoteChannelType(temporaryChannelId, channelFlags, channelType_opt, d.localFeatures, d.remoteFeatures) match { + case Right(channelType) => + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, fundingAmount).andThen { + case Success(localParams) => selfRef ! SpawnChannelNonInitiator(open, ChannelConfig.standard, channelType, localParams) + case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(temporaryChannelId, "channel creation failed"), d.peerConnection) + } + case Left(ex) => + log.warning("ignoring remote channel open: {}", ex.getMessage) + val err = Error(temporaryChannelId, ex.getMessage) + self ! Peer.OutgoingMessage(err, d.peerConnection) + } + } + def validateRemoteChannelType(temporaryChannelId: ByteVector32, channelFlags: ChannelFlags, remoteChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, SupportedChannelType] = { remoteChannelType_opt match { // remote explicitly specifies a channel type: we check whether we want to allow it From 56671ab9eff04288fbffaeb6dcb6fc2a487ebe35 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 9 Aug 2022 16:44:23 +0200 Subject: [PATCH 4/4] Rename params validation methods --- .../main/scala/fr/acinq/eclair/channel/Helpers.scala | 12 ++++++------ .../eclair/channel/fsm/ChannelOpenDualFunded.scala | 4 ++-- .../eclair/channel/fsm/ChannelOpenSingleFunder.scala | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) 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 e8dd5a309a..1da9e1906e 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 @@ -80,8 +80,8 @@ object Helpers { } } - /** Called by the fundee of a singler-funded channel. */ - def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + /** Called by the fundee of a single-funded channel. */ + def validateParamsSingleFundedFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) @@ -131,7 +131,7 @@ object Helpers { } /** Called by the non-initiator of a dual-funded channel. */ - def validateParamsNonInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, open: OpenDualFundedChannel, remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsDualFundedNonInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, open: OpenDualFundedChannel, remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) @@ -179,8 +179,8 @@ object Helpers { } } - /** Called by the funder of a single-funder channel. */ - def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + /** Called by the funder of a single-funded channel. */ + def validateParamsSingleFundedFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { case Some(t) => return Left(t) case None => // we agree on channel type @@ -214,7 +214,7 @@ object Helpers { } /** Called by the initiator of a dual-funded channel. */ - def validateParamsInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsDualFundedInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { case Some(t) => return Left(t) case None => // we agree on channel type diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index cd719ce43f..77f3230de1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -103,7 +103,7 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions { case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => import d.init.{localParams, remoteInit} - Helpers.validateParamsNonInitiator(nodeParams, d.init.channelType, open, remoteNodeId, localParams.initFeatures, remoteInit.features) match { + Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, remoteNodeId, localParams.initFeatures, remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate))) @@ -165,7 +165,7 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { when(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL)(handleExceptions { case Event(accept: AcceptDualFundedChannel, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => import d.init.{localParams, remoteInit} - Helpers.validateParamsInitiator(nodeParams, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match { + Helpers.validateParamsDualFundedInitiator(nodeParams, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) handleLocalError(t, d, Some(accept)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 64d0aa51a0..7d83bd71a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -111,7 +111,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_CHANNEL_NON_INITIATOR(_, _, _, localParams, _, remoteInit, channelConfig, channelType))) => - Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { + Helpers.validateParamsSingleFundedFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None)) @@ -168,7 +168,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, fundingSatoshis, _, commitTxFeerate, fundingTxFeerate, pushMsat_opt, localParams, _, remoteInit, _, channelConfig, channelType), open)) => - Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { + Helpers.validateParamsSingleFundedFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) handleLocalError(t, d, Some(accept))