mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-21 14:04:10 +01:00
Explicit channel type in channel open (#1867)
Add support for https://github.com/lightningnetwork/lightning-rfc/pull/880 This lets node operators open a channel with different features than what the implicit choice based on activated features would use.
This commit is contained in:
parent
275581df96
commit
59ccf3427a
31 changed files with 576 additions and 197 deletions
|
@ -90,7 +90,7 @@ trait Eclair {
|
|||
|
||||
def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse]
|
||||
def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse]
|
||||
|
||||
def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]]
|
||||
|
||||
|
@ -177,13 +177,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
|
|||
(appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String]
|
||||
}
|
||||
|
||||
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
|
||||
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
|
||||
// we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response
|
||||
val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds))
|
||||
(appKit.switchboard ? Peer.OpenChannel(
|
||||
remoteNodeId = nodeId,
|
||||
fundingSatoshis = fundingAmount,
|
||||
pushMsat = pushAmount_opt.getOrElse(0 msat),
|
||||
channelType_opt = channelType_opt,
|
||||
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
|
||||
channelFlags = flags_opt.map(_.toByte),
|
||||
timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse]
|
||||
|
|
|
@ -18,9 +18,8 @@ package fr.acinq.eclair.blockchain.fee
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Satoshi
|
||||
import fr.acinq.eclair.Features
|
||||
import fr.acinq.eclair.blockchain.CurrentFeerates
|
||||
import fr.acinq.eclair.channel.ChannelFeatures
|
||||
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes, SupportedChannelType}
|
||||
|
||||
trait FeeEstimator {
|
||||
// @formatter:off
|
||||
|
@ -33,16 +32,19 @@ case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutua
|
|||
|
||||
case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
|
||||
/**
|
||||
* @param channelFeatures permanent channel features
|
||||
* @param channelType channel type
|
||||
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
|
||||
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
|
||||
* @return true if the difference between proposed and reference fee rates is too high.
|
||||
*/
|
||||
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
|
||||
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
|
||||
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
|
||||
} else {
|
||||
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
|
||||
def isFeeDiffTooHigh(channelType: SupportedChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
|
||||
channelType match {
|
||||
case ChannelTypes.Standard =>
|
||||
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
|
||||
case ChannelTypes.StaticRemoteKey =>
|
||||
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
|
||||
case ChannelTypes.AnchorOutputs =>
|
||||
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,15 +63,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
|
|||
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
|
||||
*
|
||||
* @param remoteNodeId nodeId of our channel peer
|
||||
* @param channelFeatures permanent channel features
|
||||
* @param channelType channel type
|
||||
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
|
||||
*/
|
||||
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
|
||||
def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: ChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
|
||||
val networkFeerate = currentFeerates_opt match {
|
||||
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
|
||||
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
|
||||
}
|
||||
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
|
||||
if (channelType == ChannelTypes.AnchorOutputs) {
|
||||
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
|
||||
} else {
|
||||
networkFeerate
|
||||
|
|
|
@ -195,7 +195,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
startWith(WAIT_FOR_INIT_INTERNAL, Nothing)
|
||||
|
||||
when(WAIT_FOR_INIT_INTERNAL)(handleExceptions {
|
||||
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, channelConfig, channelFeatures), Nothing) =>
|
||||
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) =>
|
||||
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw)))
|
||||
activeConnection = remote
|
||||
txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId)
|
||||
|
@ -203,7 +203,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
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 (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
|
||||
val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
|
||||
val open = OpenChannel(nodeParams.chainHash,
|
||||
temporaryChannelId = temporaryChannelId,
|
||||
fundingSatoshis = fundingSatoshis,
|
||||
|
@ -222,7 +222,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
|
||||
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
|
||||
channelFlags = channelFlags,
|
||||
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
|
||||
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.isFunder =>
|
||||
|
@ -337,18 +340,17 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
})
|
||||
|
||||
when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions {
|
||||
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelFeatures))) =>
|
||||
log.info("received OpenChannel={}", open)
|
||||
Helpers.validateParamsFundee(nodeParams, localParams.initFeatures, channelFeatures, open, remoteNodeId) match {
|
||||
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, 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(remoteShutdownScript) =>
|
||||
case Right((channelFeatures, remoteShutdownScript)) =>
|
||||
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
|
||||
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
|
||||
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
|
||||
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
|
||||
// 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 (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
|
||||
val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
|
||||
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
|
||||
dustLimitSatoshis = localParams.dustLimit,
|
||||
maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat,
|
||||
|
@ -363,7 +365,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
|
||||
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
|
||||
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
|
||||
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
|
||||
tlvStream = TlvStream(
|
||||
ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
|
||||
ChannelTlv.ChannelTypeTlv(channelType)
|
||||
))
|
||||
val remoteParams = RemoteParams(
|
||||
nodeId = remoteNodeId,
|
||||
dustLimit = open.dustLimitSatoshis,
|
||||
|
@ -391,11 +396,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
})
|
||||
|
||||
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
|
||||
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) =>
|
||||
log.info(s"received AcceptChannel=$accept")
|
||||
Helpers.validateParamsFunder(nodeParams, channelFeatures, open, accept) match {
|
||||
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) =>
|
||||
Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match {
|
||||
case Left(t) => handleLocalError(t, d, Some(accept))
|
||||
case Right(remoteShutdownScript) =>
|
||||
case Right((channelFeatures, remoteShutdownScript)) =>
|
||||
val remoteParams = RemoteParams(
|
||||
nodeId = remoteNodeId,
|
||||
dustLimit = accept.dustLimitSatoshis,
|
||||
|
@ -889,7 +893,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey, _), d: DATA_NORMAL) =>
|
||||
d.commitments.getRemoteShutdownScript(remoteScriptPubKey) match {
|
||||
case Left(e) =>
|
||||
log.warning("they sent an invalid closing script")
|
||||
log.warning(s"they sent an invalid closing script: ${e.getMessage}")
|
||||
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
|
||||
stay() sending Warning(d.channelId, "invalid closing script")
|
||||
case Right(remoteShutdownScript) =>
|
||||
|
@ -1681,7 +1685,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
|
||||
if (d.commitments.localParams.isFunder && !shutdownInProgress) {
|
||||
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, None)
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None)
|
||||
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
|
||||
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
|
||||
}
|
||||
|
@ -1972,11 +1976,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
}
|
||||
|
||||
private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
|
||||
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
|
||||
val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
|
||||
val shouldClose = !d.commitments.localParams.isFunder &&
|
||||
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
|
||||
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
|
||||
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
|
||||
if (shouldUpdateFee) {
|
||||
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
|
||||
|
@ -1996,11 +2000,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
* @return
|
||||
*/
|
||||
private def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = {
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
|
||||
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
|
||||
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
|
||||
// if the network fees are too high we risk to not be able to confirm our current commitment
|
||||
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
|
||||
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
|
||||
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
|
||||
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
|
||||
if (shouldClose) {
|
||||
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {
|
||||
|
|
|
@ -87,13 +87,13 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
|
|||
remoteInit: Init,
|
||||
channelFlags: Byte,
|
||||
channelConfig: ChannelConfig,
|
||||
channelFeatures: ChannelFeatures)
|
||||
channelType: SupportedChannelType)
|
||||
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
|
||||
localParams: LocalParams,
|
||||
remote: ActorRef,
|
||||
remoteInit: Init,
|
||||
channelConfig: ChannelConfig,
|
||||
channelFeatures: ChannelFeatures)
|
||||
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)
|
||||
|
|
|
@ -40,6 +40,7 @@ case class InvalidChainHash (override val channelId: Byte
|
|||
case class InvalidFundingAmount (override val channelId: ByteVector32, fundingAmount: Satoshi, min: Satoshi, max: Satoshi) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingAmount (min=$min max=$max)")
|
||||
case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)")
|
||||
case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
|
||||
case class InvalidChannelType (override val channelId: ByteVector32, ourChannelType: ChannelType, theirChannelType: ChannelType) extends ChannelException(channelId, s"invalid channel_type=$theirChannelType, expected channel_type=$ourChannelType")
|
||||
case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)")
|
||||
case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)")
|
||||
case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve")
|
||||
|
|
|
@ -18,7 +18,7 @@ package fr.acinq.eclair.channel
|
|||
|
||||
import fr.acinq.eclair.Features.{AnchorOutputs, OptionUpfrontShutdownScript, StaticRemoteKey, Wumbo}
|
||||
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
|
||||
import fr.acinq.eclair.{Feature, Features}
|
||||
import fr.acinq.eclair.{Feature, FeatureSupport, Features}
|
||||
|
||||
/**
|
||||
* Created by t-bast on 24/06/2021.
|
||||
|
@ -31,11 +31,6 @@ import fr.acinq.eclair.{Feature, Features}
|
|||
*/
|
||||
case class ChannelFeatures(activated: Set[Feature]) {
|
||||
|
||||
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
|
||||
val paysDirectlyToWallet: Boolean = {
|
||||
hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs)
|
||||
}
|
||||
|
||||
/** Format of the channel transactions. */
|
||||
val commitmentFormat: CommitmentFormat = {
|
||||
if (hasFeature(AnchorOutputs)) {
|
||||
|
@ -45,6 +40,18 @@ case class ChannelFeatures(activated: Set[Feature]) {
|
|||
}
|
||||
}
|
||||
|
||||
val channelType: SupportedChannelType = {
|
||||
if (hasFeature(AnchorOutputs)) {
|
||||
ChannelTypes.AnchorOutputs
|
||||
} else if (hasFeature(StaticRemoteKey)) {
|
||||
ChannelTypes.StaticRemoteKey
|
||||
} else {
|
||||
ChannelTypes.Standard
|
||||
}
|
||||
}
|
||||
|
||||
val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet
|
||||
|
||||
def hasFeature(feature: Feature): Boolean = activated.contains(feature)
|
||||
|
||||
override def toString: String = activated.mkString(",")
|
||||
|
@ -55,18 +62,69 @@ object ChannelFeatures {
|
|||
|
||||
def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))
|
||||
|
||||
/** Pick the channel features that should be used based on local and remote feature bits. */
|
||||
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
|
||||
/** Enrich the channel type with other permanent features that will be applied to the channel. */
|
||||
def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
|
||||
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
|
||||
// such as option_dataloss_protect or option_shutdown_anysegwit.
|
||||
val availableFeatures = Set[Feature](
|
||||
StaticRemoteKey,
|
||||
Wumbo,
|
||||
AnchorOutputs,
|
||||
OptionUpfrontShutdownScript
|
||||
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
|
||||
|
||||
ChannelFeatures(availableFeatures)
|
||||
val availableFeatures: Seq[Feature] = Seq(Wumbo, OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
|
||||
val allFeatures = channelType.features.toSeq ++ availableFeatures
|
||||
ChannelFeatures(allFeatures: _*)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */
|
||||
sealed trait ChannelType {
|
||||
/** Features representing that channel type. */
|
||||
def features: Set[Feature]
|
||||
}
|
||||
|
||||
sealed trait SupportedChannelType extends ChannelType {
|
||||
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
|
||||
def paysDirectlyToWallet: Boolean
|
||||
}
|
||||
|
||||
object ChannelTypes {
|
||||
|
||||
// @formatter:off
|
||||
case object Standard extends SupportedChannelType {
|
||||
override def features: Set[Feature] = Set.empty
|
||||
override def paysDirectlyToWallet: Boolean = false
|
||||
override def toString: String = "standard"
|
||||
}
|
||||
case object StaticRemoteKey extends SupportedChannelType {
|
||||
override def features: Set[Feature] = Set(Features.StaticRemoteKey)
|
||||
override def paysDirectlyToWallet: Boolean = true
|
||||
override def toString: String = "static_remotekey"
|
||||
}
|
||||
case object AnchorOutputs extends SupportedChannelType {
|
||||
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs)
|
||||
override def paysDirectlyToWallet: Boolean = false
|
||||
override def toString: String = "anchor_outputs"
|
||||
}
|
||||
case class UnsupportedChannelType(featureBits: Features) extends ChannelType {
|
||||
override def features: Set[Feature] = featureBits.activated.keySet
|
||||
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
// NB: Bolt 2: features must exactly match in order to identify a channel type.
|
||||
def fromFeatures(features: Features): ChannelType = features match {
|
||||
case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory) => AnchorOutputs
|
||||
case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory) => StaticRemoteKey
|
||||
case f if f == Features.empty => Standard
|
||||
case _ => UnsupportedChannelType(features)
|
||||
}
|
||||
|
||||
/** Pick the channel type based on local and remote feature bits. */
|
||||
def pickChannelType(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = {
|
||||
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
|
||||
AnchorOutputs
|
||||
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
|
||||
StaticRemoteKey
|
||||
} else {
|
||||
Standard
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -113,10 +113,9 @@ case class Commitments(channelId: ByteVector32,
|
|||
(channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), remoteParams.shutdownScript) match {
|
||||
case (false, _) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId))
|
||||
case (false, _) => Right(remoteScriptPubKey)
|
||||
case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => {
|
||||
case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) =>
|
||||
// this is a special case: they set option_upfront_shutdown_script but did not provide a script in their open/accept message
|
||||
Left(InvalidFinalScript(channelId))
|
||||
}
|
||||
case (true, None) => Right(remoteScriptPubKey)
|
||||
case (true, Some(script)) if script != remoteScriptPubKey => Left(InvalidFinalScript(channelId))
|
||||
case (true, Some(script)) => Right(script)
|
||||
|
@ -200,6 +199,8 @@ case class Commitments(channelId: ByteVector32,
|
|||
|
||||
val commitmentFormat: CommitmentFormat = channelFeatures.commitmentFormat
|
||||
|
||||
val channelType: SupportedChannelType = channelFeatures.channelType
|
||||
|
||||
val localNodeId: PublicKey = localParams.nodeId
|
||||
|
||||
val remoteNodeId: PublicKey = remoteParams.nodeId
|
||||
|
@ -338,9 +339,9 @@ object Commitments {
|
|||
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
|
||||
// we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs
|
||||
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelType, commitments.capacity, None)
|
||||
val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
|
||||
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, feerate)) match {
|
||||
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, feerate)) match {
|
||||
case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate))
|
||||
case None =>
|
||||
}
|
||||
|
@ -402,9 +403,9 @@ object Commitments {
|
|||
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
|
||||
// we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs
|
||||
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelType, commitments.capacity, None)
|
||||
val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
|
||||
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, feerate)) match {
|
||||
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, feerate)) match {
|
||||
case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate))
|
||||
case None =>
|
||||
}
|
||||
|
@ -551,9 +552,9 @@ object Commitments {
|
|||
Left(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw))
|
||||
} else {
|
||||
Metrics.RemoteFeeratePerKw.withoutTags().record(fee.feeratePerKw.toLong)
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
|
||||
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelType, commitments.capacity, None)
|
||||
log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toLong.toDouble / localFeeratePerKw.toLong)
|
||||
if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) {
|
||||
if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) {
|
||||
Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw))
|
||||
} else {
|
||||
// NB: we check that the funder can afford this new fee even if spec allows to do it at next signature
|
||||
|
|
|
@ -79,8 +79,11 @@ object Helpers {
|
|||
nodeParams.minDepthBlocks.max(blocksToReachFunding)
|
||||
}
|
||||
|
||||
def extractShutdownScript(channelId: ByteVector32, channelFeatures: ChannelFeatures, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] =
|
||||
extractShutdownScript(channelId, channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), channelFeatures.hasFeature(Features.ShutdownAnySegwit), upfrontShutdownScript_opt)
|
||||
def extractShutdownScript(channelId: ByteVector32, localFeatures: Features, remoteFeatures: Features, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
|
||||
val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.OptionUpfrontShutdownScript)
|
||||
val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit)
|
||||
extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt)
|
||||
}
|
||||
|
||||
def extractShutdownScript(channelId: ByteVector32, hasOptionUpfrontShutdownScript: Boolean, allowAnySegwit: Boolean, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
|
||||
(hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match {
|
||||
|
@ -96,7 +99,7 @@ object Helpers {
|
|||
/**
|
||||
* Called by the fundee
|
||||
*/
|
||||
def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Option[ByteVector]] = {
|
||||
def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features): 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))
|
||||
|
@ -104,7 +107,7 @@ object Helpers {
|
|||
if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis))
|
||||
|
||||
// BOLT #2: Channel funding limits
|
||||
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !initFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING))
|
||||
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING))
|
||||
|
||||
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
|
||||
if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi))
|
||||
|
@ -131,8 +134,8 @@ 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, channelFeatures, open.fundingSatoshis, None)
|
||||
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
|
||||
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_DUSTLIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT))
|
||||
|
@ -144,13 +147,25 @@ object Helpers {
|
|||
val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
|
||||
|
||||
extractShutdownScript(open.temporaryChannelId, channelFeatures, open.upfrontShutdownScript_opt)
|
||||
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures)
|
||||
extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the funder
|
||||
*/
|
||||
def validateParamsFunder(nodeParams: NodeParams, channelFeatures: ChannelFeatures, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Option[ByteVector]] = {
|
||||
def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, remoteFeatures: Features, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
|
||||
accept.channelType_opt match {
|
||||
case None if channelType != ChannelTypes.pickChannelType(localFeatures, remoteFeatures) =>
|
||||
// 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.pickChannelType(localFeatures, remoteFeatures)))
|
||||
case Some(theirChannelType) if accept.channelType_opt != open.channelType_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))
|
||||
case _ => // we agree on channel type
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -177,7 +192,8 @@ object Helpers {
|
|||
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
|
||||
|
||||
extractShutdownScript(accept.temporaryChannelId, channelFeatures, accept.upfrontShutdownScript_opt)
|
||||
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures)
|
||||
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,6 +29,7 @@ import fr.acinq.eclair._
|
|||
import fr.acinq.eclair.blockchain.EclairWallet
|
||||
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
|
||||
import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.io.Monitoring.Metrics
|
||||
import fr.acinq.eclair.io.PeerConnection.KillReason
|
||||
|
@ -137,14 +138,15 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
|
|||
stay()
|
||||
} else {
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val channelFeatures = ChannelFeatures.pickChannelFeatures(d.localFeatures, d.remoteFeatures)
|
||||
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelFeatures, funder = true, c.fundingSatoshis, origin_opt = Some(sender()))
|
||||
// If a channel type was provided, we directly use it instead of computing it based on local and remote features.
|
||||
val channelType = c.channelType_opt.getOrElse(ChannelTypes.pickChannelType(d.localFeatures, d.remoteFeatures))
|
||||
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = true, c.fundingSatoshis, origin_opt = Some(sender()))
|
||||
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, channelFeatures, c.fundingSatoshis, None)
|
||||
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 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.channelFlags), channelConfig, channelFeatures)
|
||||
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.channelFlags), channelConfig, channelType)
|
||||
stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
|
||||
}
|
||||
|
||||
|
@ -152,13 +154,32 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
|
|||
d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match {
|
||||
case None =>
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val channelFeatures = ChannelFeatures.pickChannelFeatures(d.localFeatures, d.remoteFeatures)
|
||||
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelFeatures, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None)
|
||||
val temporaryChannelId = msg.temporaryChannelId
|
||||
log.info(s"accepting a new channel with temporaryChannelId=$temporaryChannelId localParams=$localParams")
|
||||
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelFeatures)
|
||||
channel ! msg
|
||||
stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
|
||||
val defaultChannelType = ChannelTypes.pickChannelType(d.localFeatures, d.remoteFeatures)
|
||||
val chosenChannelType: Either[InvalidChannelType, SupportedChannelType] = msg.channelType_opt match {
|
||||
case None => Right(defaultChannelType)
|
||||
case Some(proposedChannelType: UnsupportedChannelType) => Left(InvalidChannelType(msg.temporaryChannelId, defaultChannelType, proposedChannelType))
|
||||
case Some(proposedChannelType: SupportedChannelType) =>
|
||||
// We ensure that we support the features necessary for this channel type.
|
||||
val featuresSupported = proposedChannelType.features.forall(f => d.localFeatures.hasFeature(f))
|
||||
if (featuresSupported) {
|
||||
Right(proposedChannelType)
|
||||
} else {
|
||||
Left(InvalidChannelType(msg.temporaryChannelId, defaultChannelType, proposedChannelType))
|
||||
}
|
||||
}
|
||||
chosenChannelType match {
|
||||
case Right(channelType) =>
|
||||
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None)
|
||||
val temporaryChannelId = msg.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 ! msg
|
||||
stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
|
||||
case Left(ex) =>
|
||||
log.warning(s"ignoring open_channel: ${ex.getMessage}")
|
||||
d.peerConnection ! Error(msg.temporaryChannelId, ex.getMessage)
|
||||
stay()
|
||||
}
|
||||
case Some(_) =>
|
||||
log.warning(s"ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}")
|
||||
stay()
|
||||
|
@ -300,8 +321,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
|
|||
s(e)
|
||||
}
|
||||
|
||||
def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = {
|
||||
val (finalScript, walletStaticPaymentBasepoint) = if (channelFeatures.paysDirectlyToWallet) {
|
||||
def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = {
|
||||
val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) {
|
||||
val walletKey = Helpers.getWalletPaymentBasepoint(wallet)
|
||||
(Script.write(Script.pay2wpkh(walletKey)), Some(walletKey))
|
||||
} else {
|
||||
|
@ -409,7 +430,7 @@ object Peer {
|
|||
}
|
||||
|
||||
case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful
|
||||
case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful {
|
||||
case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful {
|
||||
require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis")
|
||||
require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive")
|
||||
require(pushMsat >= 0.msat, s"pushMsat must be positive")
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
|
||||
package fr.acinq.eclair.wire.protocol
|
||||
|
||||
import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes}
|
||||
import fr.acinq.eclair.wire.protocol.CommonCodecs._
|
||||
import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream
|
||||
import fr.acinq.eclair.{FeatureSupport, Features, UInt64}
|
||||
import scodec.Codec
|
||||
import scodec.bits.ByteVector
|
||||
import scodec.codecs._
|
||||
|
@ -30,10 +31,20 @@ sealed trait AcceptChannelTlv extends Tlv
|
|||
object ChannelTlv {
|
||||
|
||||
/** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */
|
||||
case class UpfrontShutdownScript(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv {
|
||||
case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv {
|
||||
val isEmpty: Boolean = script.isEmpty
|
||||
}
|
||||
|
||||
val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScriptTlv]
|
||||
|
||||
/** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */
|
||||
case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv
|
||||
|
||||
val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap(
|
||||
b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b))),
|
||||
tlv => Features(tlv.channelType.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
object OpenChannelTlv {
|
||||
|
@ -41,7 +52,8 @@ object OpenChannelTlv {
|
|||
import ChannelTlv._
|
||||
|
||||
val openTlvCodec: Codec[TlvStream[OpenChannelTlv]] = tlvStream(discriminated[OpenChannelTlv].by(varint)
|
||||
.typecase(UInt64(0), variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScript])
|
||||
.typecase(UInt64(0), upfrontShutdownScriptCodec)
|
||||
.typecase(UInt64(1), channelTypeCodec)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -51,7 +63,8 @@ object AcceptChannelTlv {
|
|||
import ChannelTlv._
|
||||
|
||||
val acceptTlvCodec: Codec[TlvStream[AcceptChannelTlv]] = tlvStream(discriminated[AcceptChannelTlv].by(varint)
|
||||
.typecase(UInt64(0), variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScript])
|
||||
.typecase(UInt64(0), upfrontShutdownScriptCodec)
|
||||
.typecase(UInt64(1), channelTypeCodec)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.google.common.base.Charsets
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
|
||||
import fr.acinq.eclair.channel.ChannelType
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -103,7 +104,8 @@ case class OpenChannel(chainHash: ByteVector32,
|
|||
firstPerCommitmentPoint: PublicKey,
|
||||
channelFlags: Byte,
|
||||
tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash {
|
||||
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script)
|
||||
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script)
|
||||
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
|
||||
}
|
||||
|
||||
case class AcceptChannel(temporaryChannelId: ByteVector32,
|
||||
|
@ -121,7 +123,8 @@ case class AcceptChannel(temporaryChannelId: ByteVector32,
|
|||
htlcBasepoint: PublicKey,
|
||||
firstPerCommitmentPoint: PublicKey,
|
||||
tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId {
|
||||
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script)
|
||||
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script)
|
||||
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
|
||||
}
|
||||
|
||||
case class FundingCreated(temporaryChannelId: ByteVector32,
|
||||
|
|
|
@ -91,14 +91,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
|
|||
val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87")
|
||||
|
||||
// standard conversion
|
||||
eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), flags_opt = None, openTimeout_opt = None)
|
||||
eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), flags_opt = None, openTimeout_opt = None)
|
||||
val open = switchboard.expectMsgType[OpenChannel]
|
||||
assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat)))
|
||||
|
||||
// check that minimum fee rate of 253 sat/bw is used
|
||||
eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), flags_opt = None, openTimeout_opt = None)
|
||||
eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), flags_opt = None, openTimeout_opt = None)
|
||||
val open1 = switchboard.expectMsgType[OpenChannel]
|
||||
assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw))
|
||||
assert(open1.channelType_opt === Some(ChannelTypes.StaticRemoteKey))
|
||||
}
|
||||
|
||||
test("call send with passing correct arguments") { f =>
|
||||
|
|
|
@ -19,8 +19,8 @@ package fr.acinq.eclair.blockchain.fee
|
|||
import fr.acinq.bitcoin.SatoshiLong
|
||||
import fr.acinq.eclair.TestConstants.TestFeeEstimator
|
||||
import fr.acinq.eclair.blockchain.CurrentFeerates
|
||||
import fr.acinq.eclair.channel.ChannelFeatures
|
||||
import fr.acinq.eclair.{FeatureSupport, Features, randomKey}
|
||||
import fr.acinq.eclair.channel.ChannelTypes
|
||||
import fr.acinq.eclair.randomKey
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
|
||||
class FeeEstimatorSpec extends AnyFunSuite {
|
||||
|
@ -36,19 +36,19 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
|||
|
||||
test("get commitment feerate") {
|
||||
val feeEstimator = new TestFeeEstimator()
|
||||
val channelFeatures = ChannelFeatures()
|
||||
val channelType = ChannelTypes.Standard
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat)), Map.empty)
|
||||
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat)))
|
||||
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelFeatures, 100000 sat, None) === FeeratePerKw(5000 sat))
|
||||
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) === FeeratePerKw(5000 sat))
|
||||
|
||||
val currentFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(4000 sat)))
|
||||
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelFeatures, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat))
|
||||
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat))
|
||||
}
|
||||
|
||||
test("get commitment feerate (anchor outputs)") {
|
||||
val feeEstimator = new TestFeeEstimator()
|
||||
val channelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
|
||||
val channelType = ChannelTypes.AnchorOutputs
|
||||
val defaultNodeId = randomKey().publicKey
|
||||
val defaultMaxCommitFeerate = FeeratePerKw(2500 sat)
|
||||
val overrideNodeId = randomKey().publicKey
|
||||
|
@ -56,23 +56,23 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
|||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, defaultMaxCommitFeerate), Map(overrideNodeId -> FeerateTolerance(0.5, 2.0, overrideMaxCommitFeerate)))
|
||||
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, None) === defaultMaxCommitFeerate / 2)
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate / 2)
|
||||
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2))
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, None) === defaultMaxCommitFeerate)
|
||||
assert(feeConf.getCommitmentFeerate(overrideNodeId, channelFeatures, 100000 sat, None) === overrideMaxCommitFeerate)
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate)
|
||||
assert(feeConf.getCommitmentFeerate(overrideNodeId, channelType, 100000 sat, None) === overrideMaxCommitFeerate)
|
||||
|
||||
val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
|
||||
|
||||
val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5))
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
|
||||
}
|
||||
|
||||
test("fee difference too high") {
|
||||
val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat))
|
||||
val channelFeatures = ChannelFeatures()
|
||||
val channelType = ChannelTypes.Standard
|
||||
val testCases = Seq(
|
||||
(FeeratePerKw(500 sat), FeeratePerKw(500 sat), false),
|
||||
(FeeratePerKw(500 sat), FeeratePerKw(250 sat), false),
|
||||
|
@ -85,13 +85,13 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
|||
(FeeratePerKw(250 sat), FeeratePerKw(1500 sat), true),
|
||||
)
|
||||
testCases.foreach { case (networkFeerate, proposedFeerate, expected) =>
|
||||
assert(tolerance.isFeeDiffTooHigh(channelFeatures, networkFeerate, proposedFeerate) === expected)
|
||||
assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected)
|
||||
}
|
||||
}
|
||||
|
||||
test("fee difference too high (anchor outputs)") {
|
||||
val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat))
|
||||
val channelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
|
||||
val channelType = ChannelTypes.AnchorOutputs
|
||||
val testCases = Seq(
|
||||
(FeeratePerKw(500 sat), FeeratePerKw(500 sat), false),
|
||||
(FeeratePerKw(500 sat), FeeratePerKw(2500 sat), false),
|
||||
|
@ -106,7 +106,7 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
|||
(FeeratePerKw(1000 sat), FeeratePerKw(499 sat), true),
|
||||
)
|
||||
testCases.foreach { case (networkFeerate, proposedFeerate, expected) =>
|
||||
assert(tolerance.isFeeDiffTooHigh(channelFeatures, networkFeerate, proposedFeerate) === expected)
|
||||
assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ package fr.acinq.eclair.channel
|
|||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.FeatureSupport._
|
||||
import fr.acinq.eclair.Features._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered
|
||||
import fr.acinq.eclair.channel.Helpers.Closing
|
||||
import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods
|
||||
|
@ -52,33 +54,60 @@ class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with Channe
|
|||
assert(!anchorOutputsChannel.paysDirectlyToWallet)
|
||||
}
|
||||
|
||||
test("pick channel features based on local and remote features") {
|
||||
import fr.acinq.eclair.FeatureSupport._
|
||||
import fr.acinq.eclair.Features
|
||||
import fr.acinq.eclair.Features._
|
||||
|
||||
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelFeatures: ChannelFeatures)
|
||||
test("pick channel type based on local and remote features") {
|
||||
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType)
|
||||
val testCases = Seq(
|
||||
TestCase(Features.empty, Features.empty, ChannelFeatures()),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelFeatures()),
|
||||
TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelFeatures()),
|
||||
TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelFeatures()),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelFeatures(Wumbo)),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelFeatures(StaticRemoteKey)),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelFeatures(StaticRemoteKey, Wumbo)),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)),
|
||||
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs)),
|
||||
TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features.empty, ChannelFeatures()),
|
||||
TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features(OptionUpfrontShutdownScript -> Optional), ChannelFeatures(OptionUpfrontShutdownScript)),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs, OptionUpfrontShutdownScript)),
|
||||
TestCase(Features.empty, Features.empty, ChannelTypes.Standard),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard),
|
||||
TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard),
|
||||
TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey),
|
||||
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey),
|
||||
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey),
|
||||
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs)
|
||||
)
|
||||
|
||||
for (testCase <- testCases) {
|
||||
assert(ChannelFeatures.pickChannelFeatures(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelFeatures)
|
||||
assert(ChannelTypes.pickChannelType(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType)
|
||||
}
|
||||
}
|
||||
|
||||
test("create channel type from features") {
|
||||
val validChannelTypes = Seq(
|
||||
Features.empty -> ChannelTypes.Standard,
|
||||
Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey,
|
||||
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs,
|
||||
)
|
||||
for ((features, expected) <- validChannelTypes) {
|
||||
assert(ChannelTypes.fromFeatures(features) === expected)
|
||||
}
|
||||
|
||||
val invalidChannelTypes = Seq(
|
||||
Features(Wumbo -> Optional),
|
||||
Features(StaticRemoteKey -> Optional),
|
||||
Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional),
|
||||
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional),
|
||||
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional),
|
||||
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory),
|
||||
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional),
|
||||
)
|
||||
for (features <- invalidChannelTypes) {
|
||||
assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features))
|
||||
}
|
||||
}
|
||||
|
||||
test("enrich channel type with other permanent channel features") {
|
||||
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).activated.isEmpty)
|
||||
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(Wumbo))
|
||||
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).activated === Set(Wumbo))
|
||||
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).activated === Set(StaticRemoteKey))
|
||||
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, Wumbo))
|
||||
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputs))
|
||||
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputs, Wumbo))
|
||||
}
|
||||
|
||||
case class HtlcWithPreimage(preimage: ByteVector32, htlc: UpdateAddHtlc)
|
||||
|
||||
case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, probe: TestProbe)
|
||||
|
|
|
@ -79,9 +79,9 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
|
|||
registerA ! alice
|
||||
registerB ! bob
|
||||
// no announcements
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelFeatures())
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelTypes.Standard)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelFeatures())
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
pipe ! (alice, bob)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
|
|
|
@ -114,7 +114,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
|
|||
SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer)
|
||||
}
|
||||
|
||||
def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, ChannelFeatures, LocalParams, ChannelFeatures) = {
|
||||
def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = {
|
||||
import setup._
|
||||
|
||||
val aliceInitFeatures = Alice.nodeParams.features
|
||||
|
@ -130,20 +130,19 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
|
|||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional))
|
||||
|
||||
val aliceChannelFeatures = ChannelFeatures.pickChannelFeatures(aliceInitFeatures, bobInitFeatures)
|
||||
val bobChannelFeatures = ChannelFeatures.pickChannelFeatures(bobInitFeatures, aliceInitFeatures)
|
||||
val channelType = ChannelTypes.pickChannelType(aliceInitFeatures, bobInitFeatures)
|
||||
|
||||
val aliceParams = Alice.channelParams
|
||||
.modify(_.initFeatures).setTo(aliceInitFeatures)
|
||||
.modify(_.walletStaticPaymentBasepoint).setToIf(aliceChannelFeatures.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
|
||||
.modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
|
||||
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
|
||||
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000))
|
||||
val bobParams = Bob.channelParams
|
||||
.modify(_.initFeatures).setTo(bobInitFeatures)
|
||||
.modify(_.walletStaticPaymentBasepoint).setToIf(bobChannelFeatures.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
|
||||
.modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
|
||||
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
|
||||
|
||||
(aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures)
|
||||
(aliceParams, bobParams, channelType)
|
||||
}
|
||||
|
||||
def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = {
|
||||
|
@ -151,7 +150,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
|
|||
import setup._
|
||||
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags)
|
||||
val channelFlags = if (tags.contains(ChannelStateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
|
||||
val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) {
|
||||
|
@ -162,9 +161,9 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
|
|||
|
||||
val aliceInit = Init(aliceParams.initFeatures)
|
||||
val bobInit = Init(bobParams.initFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, 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, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes)
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout
|
|||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
|
||||
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, TestConstants, TestKitBaseClass}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, TestConstants, TestKitBaseClass}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -60,13 +60,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags)
|
||||
val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType
|
||||
val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
|
||||
val aliceInit = Init(aliceParams.initFeatures)
|
||||
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, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL)
|
||||
|
@ -78,11 +80,87 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
// Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script.
|
||||
assert(accept.tlvStream === TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
|
||||
assert(accept.upfrontShutdownScript_opt === Some(ByteVector.empty))
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.Standard))
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs))
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs))
|
||||
// Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but
|
||||
// they both activated anchor outputs so it is the default choice anyway.
|
||||
bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))))
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
// Alice asked for a standard channel whereas they both support anchor outputs.
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.Standard))
|
||||
bob2alice.forward(alice, accept)
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.Standard)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.Standard))
|
||||
// Alice asked for a standard channel whereas they both support anchor outputs. Bob doesn't support explicit channel
|
||||
// type negotiation so Alice needs to abort because the channel types won't match.
|
||||
bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))))
|
||||
alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard"))
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { _ =>
|
||||
val noopWallet = new TestWallet {
|
||||
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse]().future // will never be completed
|
||||
}
|
||||
|
||||
val setup = init(Alice.nodeParams, Bob.nodeParams, wallet = noopWallet)
|
||||
import setup._
|
||||
|
||||
val channelConfig = ChannelConfig.standard
|
||||
// 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.Empty, channelConfig, ChannelTypes.AnchorOutputs)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs)
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs))
|
||||
alice2bob.forward(bob, open)
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs))
|
||||
bob2alice.forward(alice, accept)
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (invalid channel type)") { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.channelType_opt === Some(ChannelTypes.Standard))
|
||||
val invalidAccept = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs)))
|
||||
bob2alice.forward(alice, invalidAccept)
|
||||
alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard"))
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv AcceptChannel (invalid max accepted htlcs)") { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
|
@ -189,7 +267,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey))
|
||||
val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
|
||||
val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))
|
||||
bob2alice.forward(alice, accept1)
|
||||
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
|
||||
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].remoteParams.shutdownScript.isEmpty)
|
||||
|
@ -198,7 +276,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f =>
|
||||
import f._
|
||||
val accept = bob2alice.expectMsgType[AcceptChannel]
|
||||
val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.fromValidHex("deadbeef"))))
|
||||
val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef"))))
|
||||
bob2alice.forward(alice, accept1)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
|
|
@ -49,12 +49,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
|
|||
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags)
|
||||
val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType
|
||||
val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL)
|
||||
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain)))
|
||||
}
|
||||
|
@ -64,9 +66,30 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
|
|||
import f._
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
// Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script.
|
||||
assert(open.tlvStream === TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
|
||||
assert(open.upfrontShutdownScript_opt === Some(ByteVector.empty))
|
||||
// We always send a channel type, even for standard channels.
|
||||
assert(open.channelType_opt === Some(ChannelTypes.Standard))
|
||||
alice2bob.forward(bob)
|
||||
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
|
||||
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard)
|
||||
}
|
||||
|
||||
test("recv OpenChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f =>
|
||||
import f._
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs))
|
||||
alice2bob.forward(bob)
|
||||
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
|
||||
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputs)
|
||||
}
|
||||
|
||||
test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f =>
|
||||
import f._
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
assert(open.channelType_opt === Some(ChannelTypes.Standard))
|
||||
alice2bob.forward(bob)
|
||||
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
|
||||
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard)
|
||||
}
|
||||
|
||||
test("recv OpenChannel (invalid chain)") { f =>
|
||||
|
@ -228,7 +251,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
|
|||
test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f =>
|
||||
import f._
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
|
||||
val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))
|
||||
alice2bob.forward(bob, open1)
|
||||
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
|
||||
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript.isEmpty)
|
||||
|
@ -237,7 +260,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
|
|||
test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f =>
|
||||
import f._
|
||||
val open = alice2bob.expectMsgType[OpenChannel]
|
||||
val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.fromValidHex("deadbeef"))))
|
||||
val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef"))))
|
||||
alice2bob.forward(bob, open1)
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
|
|
|
@ -46,12 +46,12 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu
|
|||
val setup = init(wallet = noopWallet)
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[AcceptChannel]
|
||||
|
|
|
@ -59,13 +59,13 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
|
|||
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -57,13 +57,13 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -46,13 +46,13 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
|
|||
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -24,7 +24,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher
|
|||
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
|
||||
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, TestConstants, TestKitBaseClass}
|
||||
import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass}
|
||||
import org.scalatest.Outcome
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
|
||||
|
@ -44,14 +44,14 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
val setup = init()
|
||||
import setup._
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -64,12 +64,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
if (unconfirmedFundingTx) {
|
||||
within(30 seconds) {
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
|
||||
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
|
||||
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.Empty, channelConfig, aliceChannelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[SetChannelId]
|
||||
alice2bob.expectMsgType[OpenChannel]
|
||||
alice2bob.forward(bob)
|
||||
|
|
|
@ -155,6 +155,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit
|
|||
remoteNodeId = node2.nodeParams.nodeId,
|
||||
fundingSatoshis = fundingSatoshis,
|
||||
pushMsat = pushMsat,
|
||||
channelType_opt = None,
|
||||
fundingTxFeeratePerKw_opt = None,
|
||||
channelFlags = None,
|
||||
timeout_opt = None))
|
||||
|
|
|
@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher
|
|||
import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory
|
||||
import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler}
|
||||
import fr.acinq.eclair.wire.protocol.Init
|
||||
import fr.acinq.eclair.{Features, MilliSatoshiLong, TestKitBaseClass, TestUtils}
|
||||
import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass, TestUtils}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.{BeforeAndAfterAll, Outcome}
|
||||
|
@ -67,16 +67,16 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu
|
|||
val aliceNodeParams = Alice.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator))
|
||||
val bobNodeParams = Bob.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator))
|
||||
val channelConfig = ChannelConfig.standard
|
||||
val channelFeatures = ChannelFeatures()
|
||||
val channelType = ChannelTypes.Standard
|
||||
val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceNodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref)
|
||||
val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobNodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref)
|
||||
val aliceInit = Init(Alice.channelParams.initFeatures)
|
||||
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.Empty, channelConfig, channelFeatures)
|
||||
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelType)
|
||||
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelFeatures)
|
||||
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelType)
|
||||
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
|
||||
pipe ! (alice, bob)
|
||||
within(30 seconds) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import fr.acinq.eclair.TestConstants._
|
|||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet}
|
||||
import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.io.Peer._
|
||||
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
|
||||
|
@ -270,7 +271,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
|
|||
connect(remoteNodeId, peer, peerConnection)
|
||||
|
||||
assert(peer.stateData.channels.isEmpty)
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, 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)")
|
||||
}
|
||||
|
@ -284,7 +285,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
|
|||
connect(remoteNodeId, peer, peerConnection) // Bob doesn't support wumbo, Alice does
|
||||
|
||||
assert(peer.stateData.channels.isEmpty)
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None))
|
||||
|
||||
assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, the remote peer doesn't support wumbo")
|
||||
}
|
||||
|
@ -298,11 +299,73 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
|
|||
connect(remoteNodeId, peer, peerConnection, 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))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, 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)")
|
||||
}
|
||||
|
||||
test("don't spawn a channel if we don't support their channel type") { f =>
|
||||
import f._
|
||||
|
||||
connect(remoteNodeId, peer, peerConnection)
|
||||
assert(peer.stateData.channels.isEmpty)
|
||||
|
||||
// They only support anchor outputs and we don't.
|
||||
{
|
||||
val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))
|
||||
val open = 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, 0, openTlv)
|
||||
peerConnection.send(peer, open)
|
||||
peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard"))
|
||||
}
|
||||
// They want to use a channel type that doesn't exist in the spec.
|
||||
{
|
||||
val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(AnchorOutputs -> Optional))))
|
||||
val open = 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, 0, openTlv)
|
||||
peerConnection.send(peer, open)
|
||||
peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x200000, expected channel_type=standard"))
|
||||
}
|
||||
// They want to use a channel type we don't support yet.
|
||||
{
|
||||
val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))
|
||||
val open = 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, 0, openTlv)
|
||||
peerConnection.send(peer, open)
|
||||
peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard"))
|
||||
}
|
||||
}
|
||||
|
||||
test("use their channel type when spawning a channel", Tag("static_remotekey")) { f =>
|
||||
import f._
|
||||
|
||||
// We both support option_static_remotekey but they want to open a standard channel.
|
||||
connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional)))
|
||||
assert(peer.stateData.channels.isEmpty)
|
||||
val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))
|
||||
val open = 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, 0, openTlv)
|
||||
peerConnection.send(peer, open)
|
||||
awaitCond(peer.stateData.channels.nonEmpty)
|
||||
assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType === ChannelTypes.Standard)
|
||||
channel.expectMsg(open)
|
||||
}
|
||||
|
||||
test("use requested channel type when spawning a channel", Tag("static_remotekey")) { f =>
|
||||
import f._
|
||||
|
||||
val probe = TestProbe()
|
||||
connect(remoteNodeId, peer, peerConnection, 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f =>
|
||||
import f._
|
||||
|
||||
|
@ -313,9 +376,9 @@ 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))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None))
|
||||
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
|
||||
assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey, AnchorOutputs))
|
||||
assert(init.channelType === ChannelTypes.AnchorOutputs)
|
||||
assert(init.fundingAmount === 15000.sat)
|
||||
assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw)
|
||||
assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget))
|
||||
|
@ -326,9 +389,9 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
|
|||
|
||||
val probe = TestProbe()
|
||||
connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory)))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None))
|
||||
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
|
||||
assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey))
|
||||
assert(init.channelType === ChannelTypes.StaticRemoteKey)
|
||||
assert(init.localParams.walletStaticPaymentBasepoint.isDefined)
|
||||
assert(init.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get)))
|
||||
}
|
||||
|
@ -345,7 +408,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
|
|||
}
|
||||
val peer = TestFSMRef(new Peer(TestConstants.Alice.nodeParams, remoteNodeId, new TestWallet, channelFactory))
|
||||
connect(remoteNodeId, peer, peerConnection)
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None))
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None))
|
||||
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
|
||||
assert(init.fundingAmount === 15000.sat)
|
||||
assert(init.pushAmount === 100.msat)
|
||||
|
|
|
@ -18,8 +18,11 @@ package fr.acinq.eclair.wire.protocol
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong}
|
||||
import fr.acinq.eclair.FeatureSupport.Mandatory
|
||||
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
|
||||
import fr.acinq.eclair.channel.ChannelTypes
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
|
||||
import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._
|
||||
|
@ -154,15 +157,21 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
// legacy encoding without upfront_shutdown_script
|
||||
defaultEncoded -> defaultOpen,
|
||||
// empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))),
|
||||
defaultEncoded ++ hex"0000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))),
|
||||
// non-empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0004 01abcdef" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(hex"01abcdef"))),
|
||||
defaultEncoded ++ hex"0004 01abcdef" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"))),
|
||||
// missing upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0302002a 050102" -> defaultOpen.copy(tlvStream = TlvStream(Nil, Seq(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))),
|
||||
// empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0000 0302002a 050102" -> defaultOpen.copy(tlvStream = TlvStream(Seq(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)), Seq(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))),
|
||||
defaultEncoded ++ hex"0000 0302002a 050102" -> defaultOpen.copy(tlvStream = TlvStream(Seq(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)), Seq(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))),
|
||||
// non-empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0002 1234 0303010203" -> defaultOpen.copy(tlvStream = TlvStream(Seq(ChannelTlv.UpfrontShutdownScript(hex"1234")), Seq(GenericTlv(UInt64(3), hex"010203"))))
|
||||
defaultEncoded ++ hex"0002 1234 0303010203" -> defaultOpen.copy(tlvStream = TlvStream(Seq(ChannelTlv.UpfrontShutdownScriptTlv(hex"1234")), Seq(GenericTlv(UInt64(3), hex"010203")))),
|
||||
// empty upfront_shutdown_script + default channel type
|
||||
defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))),
|
||||
// empty upfront_shutdown_script + channel type
|
||||
defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))),
|
||||
// non-empty upfront_shutdown_script + channel type
|
||||
defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs)))
|
||||
)
|
||||
|
||||
for ((encoded, expected) <- testCases) {
|
||||
|
@ -179,6 +188,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
defaultEncoded ++ hex"00", // truncated length
|
||||
defaultEncoded ++ hex"01", // truncated length
|
||||
defaultEncoded ++ hex"0004 123456", // truncated upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0000 01040101", // truncated channel type
|
||||
defaultEncoded ++ hex"0000 02012a", // invalid tlv stream (unknown even record)
|
||||
defaultEncoded ++ hex"0000 01012a 030201", // invalid tlv stream (truncated)
|
||||
defaultEncoded ++ hex"02012a", // invalid tlv stream (unknown even record)
|
||||
|
@ -199,10 +209,12 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
val defaultEncoded = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a"
|
||||
val testCases = Map(
|
||||
defaultEncoded -> defaultAccept, // legacy encoding without upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))), // empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0004 01abcdef" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(hex"01abcdef"))), // non-empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0000 0102002a 030102" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty) :: Nil, GenericTlv(UInt64(1), hex"002a") :: GenericTlv(UInt64(3), hex"02") :: Nil)), // empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0002 1234 0303010203" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(hex"1234") :: Nil, GenericTlv(UInt64(3), hex"010203") :: Nil)), // non-empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))), // empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))), // empty upfront_shutdown_script with channel type
|
||||
defaultEncoded ++ hex"0004 01abcdef" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"))), // non-empty upfront_shutdown_script
|
||||
defaultEncoded ++ hex"0004 01abcdef" ++ hex"01021000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script with channel type
|
||||
defaultEncoded ++ hex"0000 0302002a 050102" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty) :: Nil, GenericTlv(UInt64(3), hex"002a") :: GenericTlv(UInt64(5), hex"02") :: Nil)), // empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0002 1234 0303010203" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"1234") :: Nil, GenericTlv(UInt64(3), hex"010203") :: Nil)), // non-empty upfront_shutdown_script + unknown odd tlv records
|
||||
defaultEncoded ++ hex"0303010203 05020123" -> defaultAccept.copy(tlvStream = TlvStream(Nil, GenericTlv(UInt64(3), hex"010203") :: GenericTlv(UInt64(5), hex"0123") :: Nil)) // no upfront_shutdown_script + unknown odd tlv records
|
||||
)
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo
|
|||
feerateError.setText("Fee rate must be greater than 0")
|
||||
case (Success(capacitySat), Success(pushMsat), Success(feeratePerByte_opt)) =>
|
||||
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
handlers.open(nodeUri, Some(Peer.OpenChannel(nodeUri.nodeId, capacitySat, MilliSatoshi(pushMsat), feeratePerByte_opt.map(f => FeeratePerKw(FeeratePerByte(f.sat))), Some(channelFlags), Some(30 seconds))))
|
||||
handlers.open(nodeUri, Some(Peer.OpenChannel(nodeUri.nodeId, capacitySat, MilliSatoshi(pushMsat), None, feeratePerByte_opt.map(f => FeeratePerKw(FeeratePerByte(f.sat))), Some(channelFlags), Some(30 seconds))))
|
||||
stage.close()
|
||||
case (Failure(t), _, _) =>
|
||||
logger.error(s"could not parse capacity with cause=${t.getLocalizedMessage}")
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package fr.acinq.eclair.api.handlers
|
||||
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route}
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.Satoshi
|
||||
import fr.acinq.eclair.MilliSatoshi
|
||||
|
@ -24,6 +24,7 @@ import fr.acinq.eclair.api.Service
|
|||
import fr.acinq.eclair.api.directives.EclairDirectives
|
||||
import fr.acinq.eclair.api.serde.FormParamExtractors._
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratePerByte
|
||||
import fr.acinq.eclair.channel.ChannelTypes
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
trait Channel {
|
||||
|
@ -32,11 +33,21 @@ trait Channel {
|
|||
import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization}
|
||||
|
||||
val open: Route = postRequest("open") { implicit t =>
|
||||
formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?,
|
||||
"channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) {
|
||||
(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) =>
|
||||
complete {
|
||||
eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt)
|
||||
formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) {
|
||||
(nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, channelFlags, openTimeout_opt) =>
|
||||
val (channelTypeOk, channelType_opt) = channelType match {
|
||||
case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard))
|
||||
case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey))
|
||||
case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs))
|
||||
case Some(_) => (false, None)
|
||||
case None => (true, None)
|
||||
}
|
||||
if (!channelTypeOk) {
|
||||
reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString} or ${ChannelTypes.AnchorOutputs.toString}"))
|
||||
} else {
|
||||
complete {
|
||||
eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, channelFlags, openTimeout_opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,7 +250,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||
val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e")
|
||||
|
||||
val eclair = mock[Eclair]
|
||||
eclair.open(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(ChannelOpened(channelId))
|
||||
eclair.open(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(ChannelOpened(channelId))
|
||||
val mockService = new MockService(eclair)
|
||||
|
||||
Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100002").toEntity) ~>
|
||||
|
@ -261,7 +261,49 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
|
|||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"")
|
||||
eclair.open(nodeId, 100002 sat, None, None, None, None)(any[Timeout]).wasCalled(once)
|
||||
eclair.open(nodeId, 100002 sat, None, None, None, None, None)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
|
||||
Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100000", "channelType" -> "super_dope_channel").toEntity) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
Route.seal(mockService.open) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == BadRequest)
|
||||
}
|
||||
|
||||
Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "standard").toEntity) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
Route.seal(mockService.route) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"")
|
||||
eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.Standard), None, None, None)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
|
||||
Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "static_remotekey").toEntity) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
Route.seal(mockService.route) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"")
|
||||
eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.StaticRemoteKey), None, None, None)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
|
||||
Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs").toEntity) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
Route.seal(mockService.route) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"")
|
||||
eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputs), None, None, None)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue