1
0
Fork 0
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:
Bastien Teinturier 2021-08-31 09:01:06 +02:00 committed by GitHub
parent 275581df96
commit 59ccf3427a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 576 additions and 197 deletions

View file

@ -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]

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -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")

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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))
}
/**

View file

@ -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")

View file

@ -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)
)
}

View file

@ -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,

View file

@ -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 =>

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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]

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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) {

View file

@ -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)

View file

@ -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
)

View file

@ -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}")

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}