1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-21 22:11:46 +01:00

Extensible Liquidity Ads (#2848)

* Add support for extensible liquidity ads

The initiator of `open_channel2`, `tx_init_rbf` and `splice_init` can
request funding from the remote node. The non-initiator node will:

- let the open-channel-interceptor plugin decide whether to provide
  liquidity for new channels or not, and how much
- always honor liquidity requests on existing channels (RBF and splice)
  when funding rates have been configured

Liquidity ads are included in the `node_announcement` message, which
lets buyers compare sellers and connect to sellers that provide rates
they are comfortable with. They are also included in the `init` message
which allows providing different rates to specific peers.

This implements https://github.com/lightning/bolts/pull/1153. We
currently use the temporary tlv tag 1339 while we're waiting for
feedback on the spec proposal.

* Add `channelCreationFee` to liquidity ads

Creating a new channel has an additional cost compared to adding
liquidity to an existing channel: the channel will be closed in the
future, which will require paying on-chain fees. Node operators can
include a `channel-creation-fee-satoshis` in their liquidity ads to
cover some of that future cost.

* Add liquidity purchases to the `AuditDb`

Whenever liquidity is purchased, we store it in the `AuditDb`. This lets
node operators gather useful statistics on their peers, and which ones
are actively using the liquidity that is purchased.

We store minimal information about the liquidity ads itself to be more
easily compatible with potential changes in the spec.
This commit is contained in:
Bastien Teinturier 2024-09-24 10:50:17 +02:00 committed by GitHub
parent 885b45bd75
commit cfdb0885f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1761 additions and 343 deletions

View file

@ -4,6 +4,15 @@
## Major changes
### Liquidity Ads
This release includes an early prototype for [liquidity ads](https://github.com/lightning/bolts/pull/1153).
Liquidity ads allow nodes to sell their liquidity in a trustless and decentralized manner.
Every node advertizes the rates at which they sell their liquidity, and buyers connect to sellers that offer interesting rates.
The liquidity ads specification is still under review and will likely change.
This feature isn't meant to be used on mainnet yet and is thus disabled by default.
### Update minimal version of Bitcoin Core
With this release, eclair requires using Bitcoin Core 27.1.
@ -28,6 +37,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909)
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
### Miscellaneous improvements and bug fixes

View file

@ -306,6 +306,40 @@ eclair {
update-fee-min-diff-ratio = 0.1
}
// Liquidity Ads allow remote nodes to pay us to provide them with inbound liquidity.
liquidity-ads {
// Multiple funding rates can be provided, for different funding amounts.
funding-rates = []
// Sample funding rates:
// funding-rates = [
// {
// min-funding-amount-satoshis = 100000 // minimum funding amount at this rate
// max-funding-amount-satoshis = 500000 // maximum funding amount at this rate
// // The seller can ask the buyer to pay for some of the weight of the funding transaction (for the inputs and
// // outputs added by the seller). This field contains the transaction weight (in vbytes) that the seller asks the
// // buyer to pay for. The default value matches the weight of one p2wpkh input with one p2wpkh change output.
// funding-weight = 400
// fee-base-satoshis = 500 // flat fee that we will receive every time we accept a liquidity request
// fee-basis-points = 250 // proportional fee based on the amount requested by our peer (2.5%)
// channel-creation-fee-satoshis = 2500 // flat fee that is added when creating a new channel
// },
// {
// min-funding-amount-satoshis = 500000
// max-funding-amount-satoshis = 5000000
// funding-weight = 750
// fee-base-satoshis = 1000
// fee-basis-points = 200 // 2%
// channel-creation-fee-satoshis = 2000
// }
// ]
// Multiple ways of paying the liquidity fees can be provided.
payment-types = [
// Liquidity fees must be paid from the buyer's channel balance during the transaction creation.
// This doesn't involve trust from the buyer or the seller.
"from_channel_balance"
]
}
peer-connection {
auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe
init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe

View file

@ -220,6 +220,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
pushAmount_opt = pushAmount_opt,
fundingTxFeerate_opt = fundingFeerate_opt.map(FeeratePerKw(_)),
fundingTxFeeBudget_opt = Some(fundingFeeBudget),
requestFunding_opt = None,
channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)),
timeout_opt = Some(openTimeout))
res <- (appKit.switchboard ? open).mapTo[OpenChannelResponse]
@ -228,14 +229,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = {
sendToChannelTyped(channel = Left(channelId),
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong)))
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong), requestFunding_opt = None))
}
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
sendToChannelTyped(channel = Left(channelId),
cmdBuilder = CMD_SPLICE(_,
spliceIn_opt = Some(SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))),
spliceOut_opt = None
spliceOut_opt = None,
requestFunding_opt = None,
))
}
@ -250,7 +252,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
sendToChannelTyped(channel = Left(channelId),
cmdBuilder = CMD_SPLICE(_,
spliceIn_opt = None,
spliceOut_opt = Some(SpliceOut(amount = amountOut, scriptPubKey = script))
spliceOut_opt = Some(SpliceOut(amount = amountOut, scriptPubKey = script)),
requestFunding_opt = None,
))
}

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair
import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi}
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelFlags
@ -88,6 +88,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
onionMessageConfig: OnionMessageConfig,
purgeInvoicesInterval: Option[FiniteDuration],
revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config,
willFundRates_opt: Option[LiquidityAds.WillFundRates],
peerWakeUpConfig: PeerReadyNotifier.WakeUpConfig) {
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey
@ -479,6 +480,33 @@ object NodeParams extends Logging {
val maxNoChannels = config.getInt("peer-connection.max-no-channels")
require(maxNoChannels > 0, "peer-connection.max-no-channels must be > 0")
val willFundRates_opt = {
val supportedPaymentTypes = Map(
LiquidityAds.PaymentType.FromChannelBalance.rfcName -> LiquidityAds.PaymentType.FromChannelBalance
)
val paymentTypes: Set[LiquidityAds.PaymentType] = config.getStringList("liquidity-ads.payment-types").asScala.map(s => {
supportedPaymentTypes.get(s) match {
case Some(paymentType) => paymentType
case None => throw new IllegalArgumentException(s"unknown liquidity ads payment type: $s")
}
}).toSet
val fundingRates: List[LiquidityAds.FundingRate] = config.getConfigList("liquidity-ads.funding-rates").asScala.map { r =>
LiquidityAds.FundingRate(
minAmount = r.getLong("min-funding-amount-satoshis").sat,
maxAmount = r.getLong("max-funding-amount-satoshis").sat,
fundingWeight = r.getInt("funding-weight"),
feeBase = r.getLong("fee-base-satoshis").sat,
feeProportional = r.getInt("fee-basis-points"),
channelCreationFee = r.getLong("channel-creation-fee-satoshis").sat,
)
}.toList
if (fundingRates.nonEmpty && paymentTypes.nonEmpty) {
Some(LiquidityAds.WillFundRates(fundingRates, paymentTypes))
} else {
None
}
}
NodeParams(
nodeKeyManager = nodeKeyManager,
channelKeyManager = channelKeyManager,
@ -615,6 +643,7 @@ object NodeParams extends Logging {
batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"),
interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS)
),
willFundRates_opt = willFundRates_opt,
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(
enabled = config.getBoolean("peer-wake-up.enabled"),
timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS)

View file

@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi}
import fr.acinq.eclair.channel.Origin
import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelNonInitiator}
import fr.acinq.eclair.payment.relay.PostRestartHtlcCleaner.IncomingHtlc
import fr.acinq.eclair.wire.protocol.Error
import fr.acinq.eclair.wire.protocol.{Error, LiquidityAds}
/** Custom plugin parameters. */
trait PluginParams {
@ -67,7 +67,7 @@ case class InterceptOpenChannelReceived(replyTo: ActorRef[InterceptOpenChannelRe
}
sealed trait InterceptOpenChannelResponse
case class AcceptOpenChannel(temporaryChannelId: ByteVector32, defaultParams: DefaultParams, localFundingAmount_opt: Option[Satoshi]) extends InterceptOpenChannelResponse
case class AcceptOpenChannel(temporaryChannelId: ByteVector32, defaultParams: DefaultParams, addFunding_opt: Option[LiquidityAds.AddFunding]) extends InterceptOpenChannelResponse
case class RejectOpenChannel(temporaryChannelId: ByteVector32, error: Error) extends InterceptOpenChannelResponse
// @formatter:on

View file

@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
import scodec.bits.ByteVector
@ -98,6 +98,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
fundingTxFeeBudget_opt: Option[Satoshi],
pushAmount_opt: Option[MilliSatoshi],
requireConfirmedInputs: Boolean,
requestFunding_opt: Option[LiquidityAds.RequestFunding],
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
@ -109,7 +110,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels")
}
case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32,
fundingContribution_opt: Option[Satoshi],
fundingContribution_opt: Option[LiquidityAds.AddFunding],
dualFunded: Boolean,
pushAmount_opt: Option[MilliSatoshi],
localParams: LocalParams,
@ -214,10 +215,10 @@ final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector],
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand
final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long) extends Command
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long, requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends Command
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command {
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends Command {
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)

View file

@ -18,12 +18,11 @@ package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction, TxId}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.Helpers.Closing.ClosingType
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate}
import fr.acinq.eclair.{BlockHeight, Features, ShortChannelId}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, LiquidityAds}
import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshi, ShortChannelId}
/**
* Created by PM on 17/08/2016.
@ -79,6 +78,14 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext
case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent
case class LiquidityPurchase(fundingTxId: TxId, fundingTxIndex: Long, isBuyer: Boolean, amount: Satoshi, fees: LiquidityAds.Fees, capacity: Satoshi, localContribution: Satoshi, remoteContribution: Satoshi, localBalance: MilliSatoshi, remoteBalance: MilliSatoshi, outgoingHtlcCount: Long, incomingHtlcCount: Long) {
val previousCapacity: Satoshi = capacity - localContribution - remoteContribution
val previousLocalBalance: MilliSatoshi = if (isBuyer) localBalance - localContribution + fees.total else localBalance - localContribution - fees.total
val previousRemoteBalance: MilliSatoshi = if (isBuyer) remoteBalance - remoteContribution - fees.total else remoteBalance - remoteContribution + fees.total
}
case class ChannelLiquidityPurchased(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, purchase: LiquidityPurchase) extends ChannelEvent
case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, error: ChannelError, isFatal: Boolean) extends ChannelEvent
// NB: the fee should be set to 0 when we're not paying it.

View file

@ -19,7 +19,7 @@ package fr.acinq.eclair.channel
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, Satoshi, Transaction, TxId}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, InteractiveTxMessage, UpdateAddHtlc}
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, InteractiveTxMessage, LiquidityAds, UpdateAddHtlc}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}
import scodec.bits.ByteVector
@ -51,6 +51,11 @@ case class ToSelfDelayTooHigh (override val channelId: Byte
case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserve: Satoshi, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserve too high: reserve=$channelReserve fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit")
case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve")
case class MissingLiquidityAds (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads field is missing")
case class InvalidLiquidityAdsSig (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads signature is invalid")
case class InvalidLiquidityAdsAmount (override val channelId: ByteVector32, proposed: Satoshi, min: Satoshi) extends ChannelException(channelId, s"liquidity ads funding amount is too low (expected at least $min, got $proposed)")
case class InvalidLiquidityAdsPaymentType (override val channelId: ByteVector32, proposed: LiquidityAds.PaymentType, allowed: Set[LiquidityAds.PaymentType]) extends ChannelException(channelId, s"liquidity ads ${proposed.rfcName} payment type is not supported (allowed=${allowed.map(_.rfcName).mkString(", ")})")
case class InvalidLiquidityAdsRate (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads funding rates don't match")
case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error")
case class InvalidFundingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid funding tx")
case class InvalidSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"invalid serial_id=${serialId.toByteVector.toHex}")

View file

@ -132,7 +132,14 @@ object Helpers {
}
/** Called by the non-initiator of a dual-funded channel. */
def validateParamsDualFundedNonInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, open: OpenDualFundedChannel, remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
def validateParamsDualFundedNonInitiator(nodeParams: NodeParams,
channelType: SupportedChannelType,
open: OpenDualFundedChannel,
fundingScript: ByteVector,
remoteNodeId: PublicKey,
localFeatures: Features[InitFeature],
remoteFeatures: Features[InitFeature],
addFunding_opt: Option[LiquidityAds.AddFunding]): Either[ChannelException, (ChannelFeatures, Option[ByteVector], Option[LiquidityAds.WillFundPurchase])] = {
// 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))
@ -162,7 +169,10 @@ object Helpers {
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
for {
script_opt <- extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt)
willFund_opt <- LiquidityAds.validateRequest(nodeParams.privateKey, open.temporaryChannelId, fundingScript, open.fundingFeerate, isChannelCreation = true, open.requestFunding_opt, addFunding_opt.flatMap(_.rates_opt))
} yield (channelFeatures, script_opt, willFund_opt)
}
private def validateChannelType(channelId: ByteVector32, channelType: SupportedChannelType, channelFlags: ChannelFlags, openChannelType_opt: Option[ChannelType], acceptChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Option[ChannelException] = {
@ -218,7 +228,13 @@ object Helpers {
}
/** Called by the initiator of a dual-funded channel. */
def validateParamsDualFundedInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
def validateParamsDualFundedInitiator(nodeParams: NodeParams,
remoteNodeId: PublicKey,
channelType: SupportedChannelType,
localFeatures: Features[InitFeature],
remoteFeatures: Features[InitFeature],
open: OpenDualFundedChannel,
accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector], Option[LiquidityAds.Purchase])] = {
validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match {
case Some(t) => return Left(t)
case None => // we agree on channel type
@ -240,8 +256,14 @@ object Helpers {
// MAY reject the channel.
if (accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
for {
script_opt <- extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt)
fundingScript = Funding.makeFundingPubKeyScript(open.fundingPubkey, accept.fundingPubkey)
liquidityPurchase_opt <- LiquidityAds.validateRemoteFunding(open.requestFunding_opt, remoteNodeId, accept.temporaryChannelId, fundingScript, accept.fundingAmount, open.fundingFeerate, isChannelCreation = true, accept.willFund_opt)
} yield {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
(channelFeatures, script_opt, liquidityPurchase_opt)
}
}
/**
@ -352,6 +374,8 @@ object Helpers {
object Funding {
def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey): ByteVector = write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey)))
def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {
val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2)
val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript))

View file

@ -948,38 +948,48 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
log.info("rejecting splice request: feerate too low")
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceRequest(d.channelId).getMessage)
} else {
log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}")
val parentCommitment = d.commitments.latest.commitment
val spliceAck = SpliceAck(d.channelId,
fundingContribution = 0.sat, // only remote contributes to the splice
fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey,
pushAmount = 0.msat,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding
)
val fundingParams = InteractiveTxParams(
channelId = d.channelId,
isInitiator = false,
localContribution = spliceAck.fundingContribution,
remoteContribution = msg.fundingContribution,
sharedInput_opt = Some(Multisig2of2Input(parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = Nil,
lockTime = msg.lockTime,
dustLimit = d.commitments.params.localParams.dustLimit.max(d.commitments.params.remoteParams.dustLimit),
targetFeerate = msg.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs)
)
val sessionId = randomBytes32()
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount,
wallet
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck
val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey
val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey)
LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.willFundRates_opt) match {
case Left(t) =>
log.warning("rejecting splice request with invalid liquidity ads: {}", t.getMessage)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage)
case Right(willFund_opt) =>
log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}")
val spliceAck = SpliceAck(d.channelId,
fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(0 sat),
fundingPubKey = localFundingPubKey,
pushAmount = 0.msat,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
willFund_opt = willFund_opt.map(_.willFund)
)
val fundingParams = InteractiveTxParams(
channelId = d.channelId,
isInitiator = false,
localContribution = spliceAck.fundingContribution,
remoteContribution = msg.fundingContribution,
sharedInput_opt = Some(Multisig2of2Input(parentCommitment)),
remoteFundingPubKey = msg.fundingPubKey,
localOutputs = Nil,
lockTime = msg.lockTime,
dustLimit = d.commitments.params.localParams.dustLimit.max(d.commitments.params.remoteParams.dustLimit),
targetFeerate = msg.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs)
)
val sessionId = randomBytes32()
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes),
localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount,
liquidityPurchase_opt = willFund_opt.map(_.purchase),
wallet
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck
}
}
case SpliceStatus.SpliceAborted =>
log.info("rejecting splice attempt: our previous tx_abort was not acked")
@ -1007,17 +1017,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
targetFeerate = spliceInit.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs)
)
val sessionId = randomBytes32()
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount,
wallet
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None))
val fundingScript = Funding.makeFundingPubKeyScript(spliceInit.fundingPubKey, msg.fundingPubKey)
LiquidityAds.validateRemoteFunding(spliceInit.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, isChannelCreation = false, msg.willFund_opt) match {
case Left(t) =>
log.info("rejecting splice attempt: invalid liquidity ads response ({})", t.getMessage)
cmd.replyTo ! RES_FAILURE(cmd, t)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage)
case Right(liquidityPurchase_opt) =>
val sessionId = randomBytes32()
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
sessionId,
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes),
localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount,
liquidityPurchase_opt = liquidityPurchase_opt,
wallet
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None))
}
case _ =>
log.info(s"ignoring unexpected splice_ack=$msg")
stay()
@ -2776,7 +2795,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
feerate = targetFeerate,
fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey,
pushAmount = cmd.pushAmount,
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding
requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding,
requestFunding_opt = cmd.requestFunding_opt
)
Right(spliceInit)
}

View file

@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapte
import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt}
import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel._
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs}
@ -110,8 +111,9 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
val tlvs: Set[OpenDualFundedChannelTlv] = Set(
upfrontShutdownScript_opt,
Some(ChannelTlv.ChannelTypeTlv(input.channelType)),
input.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
if (input.requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
input.requestFunding_opt.map(ChannelTlv.RequestFundingTlv),
input.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
).flatten
val open = OpenDualFundedChannel(
chainHash = nodeParams.chainHash,
@ -140,9 +142,11 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions {
case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) =>
import d.init.{localParams, remoteInit}
Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, remoteNodeId, localParams.initFeatures, remoteInit.features) match {
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey
val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey)
Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, fundingScript, remoteNodeId, localParams.initFeatures, remoteInit.features, d.init.fundingContribution_opt) match {
case Left(t) => handleLocalError(t, d, Some(open))
case Right((channelFeatures, remoteShutdownScript)) =>
case Right((channelFeatures, remoteShutdownScript, willFund_opt)) =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isOpener = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate)))
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
@ -159,13 +163,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
initFeatures = remoteInit.features,
upfrontShutdownScript_opt = remoteShutdownScript)
log.debug("remote params: {}", remoteParams)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey
val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig)
val revocationBasePoint = keyManager.revocationPoint(channelKeyPath).publicKey
// We've exchanged open_channel2 and accept_channel2, we now know the final channelId.
val channelId = Helpers.computeChannelId(open.revocationBasepoint, revocationBasePoint)
val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, localParams, remoteParams, open.channelFlags)
val localAmount = d.init.fundingContribution_opt.getOrElse(0 sat)
val localAmount = d.init.fundingContribution_opt.map(_.fundingAmount).getOrElse(0 sat)
val remoteAmount = open.fundingAmount
// At this point, the min_depth is an estimate and may change after we know exactly how our peer contributes
// to the funding transaction. Maybe they will contribute 0 satoshis to the shared output, but still add inputs
@ -175,8 +178,9 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
val tlvs: Set[AcceptDualFundedChannelTlv] = Set(
upfrontShutdownScript_opt,
Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)),
d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
if (nodeParams.channelConf.requireConfirmedInputsForDualFunding) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
willFund_opt.map(l => ChannelTlv.ProvideFundingTlv(l.willFund)),
d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
).flatten
val accept = AcceptDualFundedChannel(
temporaryChannelId = open.temporaryChannelId,
@ -218,6 +222,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
nodeParams, fundingParams,
channelParams, purpose,
localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount,
willFund_opt.map(_.purchase),
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept
@ -233,11 +238,11 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
when(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL)(handleExceptions {
case Event(accept: AcceptDualFundedChannel, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) =>
import d.init.{localParams, remoteInit}
Helpers.validateParamsDualFundedInitiator(nodeParams, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match {
Helpers.validateParamsDualFundedInitiator(nodeParams, remoteNodeId, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match {
case Left(t) =>
d.init.replyTo ! OpenChannelResponse.Rejected(t.getMessage)
handleLocalError(t, d, Some(accept))
case Right((channelFeatures, remoteShutdownScript)) =>
case Right((channelFeatures, remoteShutdownScript, liquidityPurchase_opt)) =>
// We've exchanged open_channel2 and accept_channel2, we now know the final channelId.
val channelId = Helpers.computeChannelId(d.lastSent.revocationBasepoint, accept.revocationBasepoint)
peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
@ -281,6 +286,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
nodeParams, fundingParams,
channelParams, purpose,
localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount,
liquidityPurchase_opt = liquidityPurchase_opt,
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo))
@ -500,7 +506,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate))
stay()
} else {
val txInitRbf = TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.latestFundingTx.fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding)
val txInitRbf = TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.latestFundingTx.fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding, cmd.requestFunding_opt)
stay() using d.copy(rbfStatus = RbfStatus.RbfRequested(cmd)) sending txInitRbf
}
case _ =>
@ -537,27 +543,36 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks)
stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, d.latestFundingTx.createdAt, d.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage)
} else {
log.info("our peer wants to raise the feerate of the funding transaction (previous={} target={})", d.latestFundingTx.fundingParams.targetFeerate, msg.feerate)
val fundingParams = d.latestFundingTx.fundingParams.copy(
// we don't change our funding contribution
remoteContribution = msg.fundingContribution,
lockTime = msg.lockTime,
targetFeerate = msg.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = nodeParams.channelConf.requireConfirmedInputsForDualFunding)
)
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
val toSend = Seq(
Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding)),
if (remainingRbfAttempts <= 3) Some(Warning(d.channelId, s"will accept at most ${remainingRbfAttempts - 1} future rbf attempts")) else None,
).flatten
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = None, txBuilder, remoteCommitSig = None)) sending toSend
val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.willFundRates_opt) match {
case Left(t) =>
log.warning("rejecting rbf attempt: invalid liquidity ads request ({})", t.getMessage)
stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, t.getMessage)
case Right(willFund_opt) =>
log.info("our peer wants to raise the feerate of the funding transaction (previous={} target={})", d.latestFundingTx.fundingParams.targetFeerate, msg.feerate)
// We contribute the amount of liquidity requested by our peer, if liquidity ads is active.
val fundingParams = d.latestFundingTx.fundingParams.copy(
localContribution = willFund_opt.map(_.purchase.amount).getOrElse(d.latestFundingTx.fundingParams.localContribution),
remoteContribution = msg.fundingContribution,
lockTime = msg.lockTime,
targetFeerate = msg.feerate,
requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = nodeParams.channelConf.requireConfirmedInputsForDualFunding)
)
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
liquidityPurchase_opt = willFund_opt.map(_.purchase),
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
val toSend = Seq(
Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding, willFund_opt.map(_.willFund))),
if (remainingRbfAttempts <= 3) Some(Warning(d.channelId, s"will accept at most ${remainingRbfAttempts - 1} future rbf attempts")) else None,
).flatten
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = None, txBuilder, remoteCommitSig = None)) sending toSend
}
}
case RbfStatus.RbfAborted =>
log.info("rejecting rbf attempt: our previous tx_abort was not acked")
@ -576,22 +591,31 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
cmd.replyTo ! RES_FAILURE(cmd, error)
stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, error.getMessage)
case RbfStatus.RbfRequested(cmd) =>
log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution)
val fundingParams = d.latestFundingTx.fundingParams.copy(
// we don't change our funding contribution
remoteContribution = msg.fundingContribution,
lockTime = cmd.lockTime,
targetFeerate = cmd.targetFeerate,
)
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None))
val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript
LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, cmd.targetFeerate, isChannelCreation = true, msg.willFund_opt) match {
case Left(t) =>
log.warning("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage)
cmd.replyTo ! RES_FAILURE(cmd, t)
stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, t.getMessage)
case Right(liquidityPurchase_opt) =>
log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution)
val txBuilder = context.spawnAnonymous(InteractiveTxBuilder(
randomBytes32(),
nodeParams, fundingParams,
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
liquidityPurchase_opt = liquidityPurchase_opt,
wallet))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None))
}
case _ =>
log.info("ignoring unexpected tx_ack_rbf")
stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage)

View file

@ -16,6 +16,8 @@
package fr.acinq.eclair.channel.fund
import akka.actor.typed.eventstream.EventStream
import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior}
import akka.event.LoggingAdapter
@ -163,6 +165,8 @@ object InteractiveTxBuilder {
def previousFundingAmount: Satoshi
def localCommitIndex: Long
def remoteCommitIndex: Long
def localNextHtlcId: Long
def remoteNextHtlcId: Long
def remotePerCommitmentPoint: PublicKey
def commitTxFeerate: FeeratePerKw
def fundingTxIndex: Long
@ -175,15 +179,19 @@ object InteractiveTxBuilder {
override val previousFundingAmount: Satoshi = 0 sat
override val localCommitIndex: Long = 0
override val remoteCommitIndex: Long = 0
override val localNextHtlcId: Long = 0
override val remoteNextHtlcId: Long = 0
override val fundingTxIndex: Long = 0
override val localHtlcs: Set[DirectedHtlc] = Set.empty
}
case class SpliceTx(parentCommitment: Commitment) extends Purpose {
case class SpliceTx(parentCommitment: Commitment, changes: CommitmentChanges) extends Purpose {
override val previousLocalBalance: MilliSatoshi = parentCommitment.localCommit.spec.toLocal
override val previousRemoteBalance: MilliSatoshi = parentCommitment.remoteCommit.spec.toLocal
override val previousFundingAmount: Satoshi = parentCommitment.capacity
override val localCommitIndex: Long = parentCommitment.localCommit.index
override val remoteCommitIndex: Long = parentCommitment.remoteCommit.index
override val localNextHtlcId: Long = changes.localNextHtlcId
override val remoteNextHtlcId: Long = changes.remoteNextHtlcId
override val remotePerCommitmentPoint: PublicKey = parentCommitment.remoteCommit.remotePerCommitmentPoint
override val commitTxFeerate: FeeratePerKw = parentCommitment.localCommit.spec.commitTxFeerate
override val fundingTxIndex: Long = parentCommitment.fundingTxIndex + 1
@ -199,6 +207,8 @@ object InteractiveTxBuilder {
override val previousFundingAmount: Satoshi = (previousLocalBalance + previousRemoteBalance).truncateToSatoshi
override val localCommitIndex: Long = replacedCommitment.localCommit.index
override val remoteCommitIndex: Long = replacedCommitment.remoteCommit.index
override val localNextHtlcId: Long = 0
override val remoteNextHtlcId: Long = 0
override val remotePerCommitmentPoint: PublicKey = replacedCommitment.remoteCommit.remotePerCommitmentPoint
override val commitTxFeerate: FeeratePerKw = replacedCommitment.localCommit.spec.commitTxFeerate
override val fundingTxIndex: Long = replacedCommitment.fundingTxIndex
@ -347,6 +357,7 @@ object InteractiveTxBuilder {
purpose: Purpose,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
liquidityPurchase_opt: Option[LiquidityAds.Purchase],
wallet: OnChainChannelFunder)(implicit ec: ExecutionContext): Behavior[Command] = {
Behaviors.setup { context =>
// The stash is used to buffer messages that arrive while we're funding the transaction.
@ -356,9 +367,13 @@ object InteractiveTxBuilder {
Behaviors.withMdc(Logs.mdc(remoteNodeId_opt = Some(channelParams.remoteParams.nodeId), channelId_opt = Some(fundingParams.channelId))) {
Behaviors.receiveMessagePartial {
case Start(replyTo) =>
val liquidityFee = liquidityPurchase_opt.map(l => l.paymentDetails match {
// The initiator of the interactive-tx is the liquidity buyer (if liquidity ads is used).
case LiquidityAds.PaymentDetails.FromChannelBalance => if (fundingParams.isInitiator) l.fees.total else -l.fees.total
}).getOrElse(0 sat)
// Note that pending HTLCs are ignored: splices only affect the main outputs.
val nextLocalBalance = purpose.previousLocalBalance + fundingParams.localContribution
val nextRemoteBalance = purpose.previousRemoteBalance + fundingParams.remoteContribution
val nextLocalBalance = purpose.previousLocalBalance + fundingParams.localContribution - localPushAmount + remotePushAmount - liquidityFee
val nextRemoteBalance = purpose.previousRemoteBalance + fundingParams.remoteContribution - remotePushAmount + localPushAmount + liquidityFee
if (fundingParams.fundingAmount < fundingParams.dustLimit) {
replyTo ! LocalFailure(FundingAmountTooLow(channelParams.channelId, fundingParams.fundingAmount, fundingParams.dustLimit))
Behaviors.stopped
@ -366,7 +381,7 @@ object InteractiveTxBuilder {
replyTo ! LocalFailure(InvalidFundingBalances(channelParams.channelId, fundingParams.fundingAmount, nextLocalBalance, nextRemoteBalance))
Behaviors.stopped
} else {
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context)
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, liquidityPurchase_opt, wallet, stash, context)
actor.start()
}
case Abort => Behaviors.stopped
@ -389,6 +404,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
purpose: Purpose,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
liquidityPurchase_opt: Option[LiquidityAds.Purchase],
wallet: OnChainChannelFunder,
stash: StashBuffer[InteractiveTxBuilder.Command],
context: ActorContext[InteractiveTxBuilder.Command])(implicit ec: ExecutionContext) {
@ -751,10 +767,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = {
val fundingTx = completeTx.buildUnsignedTx()
val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript)
val liquidityFee = liquidityPurchase_opt.map(l => l.paymentDetails match {
// The initiator of the interactive-tx is the liquidity buyer (if liquidity ads is used).
case LiquidityAds.PaymentDetails.FromChannelBalance => if (fundingParams.isInitiator) l.fees.total else -l.fees.total
}).getOrElse(0 sat)
Funding.makeCommitTxs(keyManager, channelParams,
fundingAmount = fundingParams.fundingAmount,
toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount,
toRemote = completeTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount,
toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFee,
toRemote = completeTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFee,
localHtlcs = purpose.localHtlcs,
purpose.commitTxFeerate,
fundingTxIndex = purpose.fundingTxIndex,
@ -782,6 +802,29 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
Behaviors.receiveMessagePartial {
case SignTransactionResult(signedTx) =>
log.info(s"interactive-tx txid=${signedTx.txId} partially signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", signedTx.tx.localInputs.length, signedTx.tx.remoteInputs.length, signedTx.tx.localOutputs.length, signedTx.tx.remoteOutputs.length)
// At this point, we're not completely sure that the transaction will succeed: if our peer doesn't send their
// commit_sig, the transaction will be aborted. But it's a best effort, because after sending our commit_sig,
// we won't store details about the liquidity purchase so we'll be unable to emit that event later. Even after
// fully signing the transaction, it may be double-spent by a force-close, which would invalidate it as well.
// The right solution is to check confirmations on the funding transaction before considering that a liquidity
// purchase is completed, which is what we do in our AuditDb.
liquidityPurchase_opt.foreach { p =>
val purchase = LiquidityPurchase(
fundingTxId = signedTx.txId,
fundingTxIndex = purpose.fundingTxIndex,
isBuyer = fundingParams.isInitiator,
amount = p.amount,
fees = p.fees,
capacity = fundingParams.fundingAmount,
localContribution = fundingParams.localContribution,
remoteContribution = fundingParams.remoteContribution,
localBalance = localCommit.spec.toLocal,
remoteBalance = localCommit.spec.toRemote,
outgoingHtlcCount = purpose.localNextHtlcId,
incomingHtlcCount = purpose.remoteNextHtlcId,
)
context.system.eventStream ! EventStream.Publish(ChannelLiquidityPurchased(replyTo.toClassic, channelParams.channelId, remoteNodeId, purchase))
}
replyTo ! Succeeded(InteractiveTxSigningSession.WaitingForSigs(fundingParams, purpose.fundingTxIndex, signedTx, Left(localCommit), remoteCommit), commitSig)
Behaviors.stopped
case WalletFailure(t) =>

View file

@ -141,6 +141,8 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
case _ => Nil
}
private val spliceInOnly = fundingParams.sharedInput_opt.nonEmpty && fundingParams.localContribution > 0.sat && fundingParams.localOutputs.isEmpty
def start(): Behavior[Command] = {
// We always double-spend all our previous inputs. It's technically overkill because we only really need to double
// spend one input of each previous tx, but it's simpler and less error-prone this way. It also ensures that in
@ -169,10 +171,21 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
replyTo ! fundingContributions
Behaviors.stopped
}
} else if (!fundingParams.isInitiator && spliceInOnly) {
// We are splicing funds in without being the initiator (most likely responding to a liquidity ads).
// We don't need to include the shared input, the other node will pay for its weight.
// We create a dummy shared output with the amount we want to splice in, and bitcoind will make sure we match that
// amount.
val sharedTxOut = TxOut(fundingParams.localContribution, fundingPubkeyScript)
val previousWalletTxIn = previousWalletInputs.map(i => TxIn(i.outPoint, ByteVector.empty, i.sequence))
val dummyTx = Transaction(2, previousWalletTxIn, Seq(sharedTxOut), fundingParams.lockTime)
fund(dummyTx, previousWalletInputs, Set.empty)
} else {
// The shared input contains funds that belong to us *and* funds that belong to our peer, so we add the previous
// funding amount to our shared output to make sure bitcoind adds what is required for our local contribution.
// We always include the shared input in our transaction and will let bitcoind make sure the target feerate is reached.
// We will later subtract the fees for that input to ensure we don't overshoot the feerate: however, if bitcoind
// doesn't add a change output, we won't be able to do so and will overpay miner fees.
// Note that if the shared output amount is smaller than the dust limit, bitcoind will reject the funding attempt.
val sharedTxOut = TxOut(purpose.previousFundingAmount + fundingParams.localContribution, fundingPubkeyScript)
val sharedTxIn = fundingParams.sharedInput_opt.toSeq.map(sharedInput => TxIn(sharedInput.info.outPoint, ByteVector.empty, 0xfffffffdL))
@ -188,7 +201,10 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
* inputs.
*/
private def fund(txNotFunded: Transaction, currentInputs: Seq[OutgoingInput], unusableInputs: Set[UnusableInput]): Behavior[Command] = {
val sharedInputWeight = fundingParams.sharedInput_opt.toSeq.map(i => i.info.outPoint -> i.weight.toLong).toMap
val sharedInputWeight = fundingParams.sharedInput_opt match {
case Some(i) if txNotFunded.txIn.exists(_.outPoint == i.info.outPoint) => Map(i.info.outPoint -> i.weight.toLong)
case _ => Map.empty[OutPoint, Long]
}
val feeBudget_opt = purpose match {
case p: FundingTx => p.feeBudget_opt
case p: PreviousTxRbf => p.feeBudget_opt
@ -249,13 +265,16 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
// By using bitcoind's fundrawtransaction we are currently paying fees for those fields, but we can fix that
// by increasing our change output accordingly.
// If we don't have a change output, we will slightly overpay the fees: fixing this is not worth the extra
// complexity of adding a change output, which would require a call to bitcoind to get a change address.
// complexity of adding a change output, which would require a call to bitcoind to get a change address and
// create a tiny change output that would most likely be unusable and costly to spend.
val outputs = changeOutput_opt match {
case Some(changeOutput) =>
val txWeightWithoutInput = Transaction(2, Nil, Seq(TxOut(fundingParams.fundingAmount, fundingPubkeyScript)), 0).weight()
val commonWeight = fundingParams.sharedInput_opt match {
case Some(sharedInput) => sharedInput.weight + txWeightWithoutInput
case None => txWeightWithoutInput
// If we are only splicing in, we didn't include the shared input in the funding transaction, but
// otherwise we did and must thus claim the corresponding fee back.
case Some(sharedInput) if !spliceInOnly => sharedInput.weight + txWeightWithoutInput
case _ => txWeightWithoutInput
}
val overpaidFees = Transactions.weight2fee(fundingParams.targetFeerate, commonWeight)
nonChangeOutputs :+ changeOutput.copy(amount = changeOutput.amount + overpaidFees)

View file

@ -44,6 +44,7 @@ trait Databases {
def peers: PeersDb
def payments: PaymentsDb
def pendingCommands: PendingCommandsDb
def liquidity: LiquidityDb
//@formatter:on
}
@ -60,6 +61,7 @@ object Databases extends Logging {
}
case class SqliteDatabases private(network: SqliteNetworkDb,
liquidity: SqliteLiquidityDb,
audit: SqliteAuditDb,
channels: SqliteChannelsDb,
peers: SqlitePeersDb,
@ -78,6 +80,7 @@ object Databases extends Logging {
jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _))
SqliteDatabases(
network = new SqliteNetworkDb(networkJdbc),
liquidity = new SqliteLiquidityDb(eclairJdbc),
audit = new SqliteAuditDb(auditJdbc),
channels = new SqliteChannelsDb(eclairJdbc),
peers = new SqlitePeersDb(eclairJdbc),
@ -89,6 +92,7 @@ object Databases extends Logging {
}
case class PostgresDatabases private(network: PgNetworkDb,
liquidity: PgLiquidityDb,
audit: PgAuditDb,
channels: PgChannelsDb,
peers: PgPeersDb,
@ -106,8 +110,7 @@ object Databases extends Logging {
auditRelayedMaxAge: FiniteDuration,
localChannelsMinCount: Int,
networkNodesMinCount: Int,
networkChannelsMinCount: Int
)
networkChannelsMinCount: Int)
def apply(hikariConfig: HikariConfig,
instanceId: UUID,
@ -149,6 +152,7 @@ object Databases extends Logging {
val databases = PostgresDatabases(
network = new PgNetworkDb,
liquidity = new PgLiquidityDb,
audit = new PgAuditDb,
channels = new PgChannelsDb,
peers = new PgPeersDb,
@ -160,7 +164,7 @@ object Databases extends Logging {
readOnlyUser_opt.foreach { readOnlyUser =>
PgUtils.inTransaction { connection =>
using(connection.createStatement()) { statement =>
val schemas = "public" :: "audit" :: "local" :: "network" :: "payments" :: Nil
val schemas = "public" :: "audit" :: "local" :: "network" :: "payments" :: "liquidity" :: Nil
schemas.foreach { schema =>
logger.info(s"granting read-only access to user=$readOnlyUser schema=$schema")
statement.executeUpdate(s"GRANT USAGE ON SCHEMA $schema TO $readOnlyUser")

View file

@ -38,6 +38,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
private val auditDb: AuditDb = nodeParams.db.audit
private val channelsDb: ChannelsDb = nodeParams.db.channels
private val liquidityDb: LiquidityDb = nodeParams.db.liquidity
context.spawn(Behaviors.supervise(RevokedHtlcInfoCleaner(channelsDb, nodeParams.revokedHtlcInfoCleanerConfig)).onFailure(SupervisorStrategy.restart), name = "revoked-htlc-info-cleaner")
@ -45,6 +46,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
context.system.eventStream.subscribe(self, classOf[PaymentFailed])
context.system.eventStream.subscribe(self, classOf[PaymentReceived])
context.system.eventStream.subscribe(self, classOf[PaymentRelayed])
context.system.eventStream.subscribe(self, classOf[ChannelLiquidityPurchased])
context.system.eventStream.subscribe(self, classOf[TransactionPublished])
context.system.eventStream.subscribe(self, classOf[TransactionConfirmed])
context.system.eventStream.subscribe(self, classOf[ChannelErrorOccurred])
@ -92,11 +94,15 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
}
auditDb.add(e)
case e: ChannelLiquidityPurchased => liquidityDb.addPurchase(e)
case e: TransactionPublished =>
log.info(s"paying mining fee=${e.miningFee} for txid=${e.tx.txid} desc=${e.desc}")
auditDb.add(e)
case e: TransactionConfirmed => auditDb.add(e)
case e: TransactionConfirmed =>
liquidityDb.setConfirmed(e.remoteNodeId, e.tx.txid)
auditDb.add(e)
case e: ChannelErrorOccurred =>
// first pattern matching level is to ignore some errors, second level is to separate between different kind of errors

View file

@ -30,16 +30,12 @@ import scala.util.{Failure, Success, Try}
case class DualDatabases(primary: Databases, secondary: Databases) extends Databases with FileBackup {
override val network: NetworkDb = DualNetworkDb(primary.network, secondary.network)
override val audit: AuditDb = DualAuditDb(primary.audit, secondary.audit)
override val channels: ChannelsDb = DualChannelsDb(primary.channels, secondary.channels)
override val peers: PeersDb = DualPeersDb(primary.peers, secondary.peers)
override val payments: PaymentsDb = DualPaymentsDb(primary.payments, secondary.payments)
override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(primary.pendingCommands, secondary.pendingCommands)
override val liquidity: LiquidityDb = DualLiquidityDb(primary.liquidity, secondary.liquidity)
/** if one of the database supports file backup, we use it */
override def backup(backupFile: File): Unit = (primary, secondary) match {
@ -411,3 +407,24 @@ case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingC
primary.listSettlementCommands()
}
}
case class DualLiquidityDb(primary: LiquidityDb, secondary: LiquidityDb) extends LiquidityDb {
private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-liquidity").build()))
override def addPurchase(liquidityPurchase: ChannelLiquidityPurchased): Unit = {
runAsync(secondary.addPurchase(liquidityPurchase))
primary.addPurchase(liquidityPurchase)
}
override def setConfirmed(remoteNodeId: PublicKey, txId: TxId): Unit = {
runAsync(secondary.setConfirmed(remoteNodeId, txId))
primary.setConfirmed(remoteNodeId, txId)
}
override def listPurchases(remoteNodeId: PublicKey): Seq[LiquidityPurchase] = {
runAsync(secondary.listPurchases(remoteNodeId))
primary.listPurchases(remoteNodeId)
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.TxId
import fr.acinq.eclair.channel.{ChannelLiquidityPurchased, LiquidityPurchase}
/**
* Created by t-bast on 13/09/2024.
*/
trait LiquidityDb {
def addPurchase(liquidityPurchase: ChannelLiquidityPurchased): Unit
def setConfirmed(remoteNodeId: PublicKey, txId: TxId): Unit
def listPurchases(remoteNodeId: PublicKey): Seq[LiquidityPurchase]
}

View file

@ -0,0 +1,121 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db.pg
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Satoshi, TxId}
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.channel.{ChannelLiquidityPurchased, LiquidityPurchase}
import fr.acinq.eclair.db.LiquidityDb
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.wire.protocol.LiquidityAds
import grizzled.slf4j.Logging
import java.sql.Timestamp
import java.time.Instant
import javax.sql.DataSource
/**
* Created by t-bast on 13/09/2024.
*/
object PgLiquidityDb {
val DB_NAME = "liquidity"
val CURRENT_VERSION = 1
}
class PgLiquidityDb(implicit ds: DataSource) extends LiquidityDb with Logging {
import PgUtils._
import ExtendedResultSet._
import PgLiquidityDb._
inTransaction { pg =>
using(pg.createStatement()) { statement =>
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE SCHEMA liquidity")
statement.executeUpdate("CREATE TABLE liquidity.purchases (tx_id TEXT NOT NULL, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, is_buyer BOOLEAN NOT NULL, amount_sat BIGINT NOT NULL, mining_fee_sat BIGINT NOT NULL, service_fee_sat BIGINT NOT NULL, funding_tx_index BIGINT NOT NULL, capacity_sat BIGINT NOT NULL, local_contribution_sat BIGINT NOT NULL, remote_contribution_sat BIGINT NOT NULL, local_balance_msat BIGINT NOT NULL, remote_balance_msat BIGINT NOT NULL, outgoing_htlc_count BIGINT NOT NULL, incoming_htlc_count BIGINT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, confirmed_at TIMESTAMP WITH TIME ZONE)")
statement.executeUpdate("CREATE INDEX liquidity_purchases_node_id_idx ON liquidity.purchases(node_id)")
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
}
setVersion(statement, DB_NAME, CURRENT_VERSION)
}
}
override def addPurchase(e: ChannelLiquidityPurchased): Unit = withMetrics("liquidity/add-purchase", DbBackends.Postgres) {
inTransaction { pg =>
using(pg.prepareStatement("INSERT INTO liquidity.purchases VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)")) { statement =>
statement.setString(1, e.purchase.fundingTxId.value.toHex)
statement.setString(2, e.channelId.toHex)
statement.setString(3, e.remoteNodeId.toHex)
statement.setBoolean(4, e.purchase.isBuyer)
statement.setLong(5, e.purchase.amount.toLong)
statement.setLong(6, e.purchase.fees.miningFee.toLong)
statement.setLong(7, e.purchase.fees.serviceFee.toLong)
statement.setLong(8, e.purchase.fundingTxIndex)
statement.setLong(9, e.purchase.capacity.toLong)
statement.setLong(10, e.purchase.localContribution.toLong)
statement.setLong(11, e.purchase.remoteContribution.toLong)
statement.setLong(12, e.purchase.localBalance.toLong)
statement.setLong(13, e.purchase.remoteBalance.toLong)
statement.setLong(14, e.purchase.outgoingHtlcCount)
statement.setLong(15, e.purchase.incomingHtlcCount)
statement.setTimestamp(16, Timestamp.from(Instant.now()))
statement.executeUpdate()
}
}
}
override def setConfirmed(remoteNodeId: PublicKey, txId: TxId): Unit = withMetrics("liquidity/set-confirmed", DbBackends.Postgres) {
inTransaction { pg =>
using(pg.prepareStatement("UPDATE liquidity.purchases SET confirmed_at=? WHERE node_id=? AND tx_id=?")) { statement =>
statement.setTimestamp(1, Timestamp.from(Instant.now()))
statement.setString(2, remoteNodeId.toHex)
statement.setString(3, txId.value.toHex)
statement.executeUpdate()
}
}
}
override def listPurchases(remoteNodeId: PublicKey): Seq[LiquidityPurchase] = withMetrics("liquidity/list-purchases", DbBackends.Postgres) {
inTransaction { pg =>
using(pg.prepareStatement("SELECT * FROM liquidity.purchases WHERE node_id=? AND confirmed_at IS NOT NULL")) { statement =>
statement.setString(1, remoteNodeId.toHex)
statement.executeQuery().map { rs =>
LiquidityPurchase(
fundingTxId = TxId(rs.getByteVector32FromHex("tx_id")),
fundingTxIndex = rs.getLong("funding_tx_index"),
isBuyer = rs.getBoolean("is_buyer"),
amount = Satoshi(rs.getLong("amount_sat")),
fees = LiquidityAds.Fees(miningFee = Satoshi(rs.getLong("mining_fee_sat")), serviceFee = Satoshi(rs.getLong("service_fee_sat"))),
capacity = Satoshi(rs.getLong("capacity_sat")),
localContribution = Satoshi(rs.getLong("local_contribution_sat")),
remoteContribution = Satoshi(rs.getLong("remote_contribution_sat")),
localBalance = MilliSatoshi(rs.getLong("local_balance_msat")),
remoteBalance = MilliSatoshi(rs.getLong("remote_balance_msat")),
outgoingHtlcCount = rs.getLong("outgoing_htlc_count"),
incomingHtlcCount = rs.getLong("incoming_htlc_count")
)
}.toSeq
}
}
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db.sqlite
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Satoshi, TxId}
import fr.acinq.eclair.channel.{ChannelLiquidityPurchased, LiquidityPurchase}
import fr.acinq.eclair.db.LiquidityDb
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.wire.protocol.LiquidityAds
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli}
import grizzled.slf4j.Logging
import java.sql.Connection
/**
* Created by t-bast on 13/09/2024.
*/
object SqliteLiquidityDb {
val DB_NAME = "liquidity"
val CURRENT_VERSION = 1
}
class SqliteLiquidityDb(val sqlite: Connection) extends LiquidityDb with Logging {
import SqliteUtils._
import ExtendedResultSet._
import SqliteLiquidityDb._
using(sqlite.createStatement(), inTransaction = true) { statement =>
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE TABLE liquidity_purchases (tx_id BLOB NOT NULL, channel_id BLOB NOT NULL, node_id BLOB NOT NULL, is_buyer BOOLEAN NOT NULL, amount_sat INTEGER NOT NULL, mining_fee_sat INTEGER NOT NULL, service_fee_sat INTEGER NOT NULL, funding_tx_index INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, local_contribution_sat INTEGER NOT NULL, remote_contribution_sat INTEGER NOT NULL, local_balance_msat INTEGER NOT NULL, remote_balance_msat INTEGER NOT NULL, outgoing_htlc_count INTEGER NOT NULL, incoming_htlc_count INTEGER NOT NULL, created_at INTEGER NOT NULL, confirmed_at INTEGER)")
statement.executeUpdate("CREATE INDEX liquidity_purchases_node_id_idx ON liquidity_purchases(node_id)")
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
}
setVersion(statement, DB_NAME, CURRENT_VERSION)
}
override def addPurchase(e: ChannelLiquidityPurchased): Unit = withMetrics("liquidity/add-purchase", DbBackends.Sqlite) {
using(sqlite.prepareStatement("INSERT INTO liquidity_purchases VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)")) { statement =>
statement.setBytes(1, e.purchase.fundingTxId.value.toArray)
statement.setBytes(2, e.channelId.toArray)
statement.setBytes(3, e.remoteNodeId.value.toArray)
statement.setBoolean(4, e.purchase.isBuyer)
statement.setLong(5, e.purchase.amount.toLong)
statement.setLong(6, e.purchase.fees.miningFee.toLong)
statement.setLong(7, e.purchase.fees.serviceFee.toLong)
statement.setLong(8, e.purchase.fundingTxIndex)
statement.setLong(9, e.purchase.capacity.toLong)
statement.setLong(10, e.purchase.localContribution.toLong)
statement.setLong(11, e.purchase.remoteContribution.toLong)
statement.setLong(12, e.purchase.localBalance.toLong)
statement.setLong(13, e.purchase.remoteBalance.toLong)
statement.setLong(14, e.purchase.outgoingHtlcCount)
statement.setLong(15, e.purchase.incomingHtlcCount)
statement.setLong(16, TimestampMilli.now().toLong)
statement.executeUpdate()
}
}
override def setConfirmed(remoteNodeId: PublicKey, txId: TxId): Unit = withMetrics("liquidity/set-confirmed", DbBackends.Sqlite) {
using(sqlite.prepareStatement("UPDATE liquidity_purchases SET confirmed_at=? WHERE node_id=? AND tx_id=?")) { statement =>
statement.setLong(1, TimestampMilli.now().toLong)
statement.setBytes(2, remoteNodeId.value.toArray)
statement.setBytes(3, txId.value.toArray)
statement.executeUpdate()
}
}
override def listPurchases(remoteNodeId: PublicKey): Seq[LiquidityPurchase] = withMetrics("liquidity/list-purchases", DbBackends.Sqlite) {
using(sqlite.prepareStatement("SELECT * FROM liquidity_purchases WHERE node_id=? AND confirmed_at IS NOT NULL")) { statement =>
statement.setBytes(1, remoteNodeId.value.toArray)
statement.executeQuery().map { rs =>
LiquidityPurchase(
fundingTxId = TxId(rs.getByteVector32("tx_id")),
fundingTxIndex = rs.getLong("funding_tx_index"),
isBuyer = rs.getBoolean("is_buyer"),
amount = Satoshi(rs.getLong("amount_sat")),
fees = LiquidityAds.Fees(miningFee = Satoshi(rs.getLong("mining_fee_sat")), serviceFee = Satoshi(rs.getLong("service_fee_sat"))),
capacity = Satoshi(rs.getLong("capacity_sat")),
localContribution = Satoshi(rs.getLong("local_contribution_sat")),
remoteContribution = Satoshi(rs.getLong("remote_contribution_sat")),
localBalance = MilliSatoshi(rs.getLong("local_balance_msat")),
remoteBalance = MilliSatoshi(rs.getLong("remote_balance_msat")),
outgoingHtlcCount = rs.getLong("outgoing_htlc_count"),
incomingHtlcCount = rs.getLong("incoming_htlc_count")
)
}.toSeq
}
}
}

View file

@ -176,8 +176,8 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
nodeParams.pluginOpenChannelInterceptor match {
case Some(plugin) => queryPlugin(plugin, request, localParams, ChannelConfig.standard, channelType)
case None =>
// NB: we don't add a contribution to the funding amount.
peer ! SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, localParams, None, request.peerConnection.toClassic)
// We don't honor liquidity ads for new channels: we let the node operator's plugin decide what to do.
peer ! SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, addFunding_opt = None, localParams, request.peerConnection.toClassic)
waitForRequest()
}
case PendingChannelsRateLimiterResponse(PendingChannelsRateLimiter.ChannelRateLimited) =>
@ -196,7 +196,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
receiveCommandMessage[QueryPluginCommands](context, "queryPlugin") {
case PluginOpenChannelResponse(pluginResponse: AcceptOpenChannel) =>
val localParams1 = updateLocalParams(localParams, pluginResponse.defaultParams)
peer ! SpawnChannelNonInitiator(request.open, channelConfig, channelType, localParams1, pluginResponse.localFundingAmount_opt, request.peerConnection.toClassic)
peer ! SpawnChannelNonInitiator(request.open, channelConfig, channelType, pluginResponse.addFunding_opt, localParams1, request.peerConnection.toClassic)
timers.cancel(PluginTimeout)
waitForRequest()
case PluginOpenChannelResponse(pluginResponse: RejectOpenChannel) =>

View file

@ -40,7 +40,7 @@ import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning}
import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, LiquidityAds, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning}
/**
* This actor represents a logical peer. There is one [[Peer]] per unique remote node id at all time.
@ -173,7 +173,7 @@ class Peer(val nodeParams: NodeParams,
val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeerates))
val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelType.commitmentFormat, c.fundingAmount)
log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=$fundingTxFeerate temporaryChannelId=$temporaryChannelId localParams=$localParams")
channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, c.channelOrigin, replyTo)
channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, c.requestFunding_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, c.channelOrigin, replyTo)
stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Event(open: protocol.OpenChannel, d: ConnectedData) =>
@ -200,7 +200,7 @@ class Peer(val nodeParams: NodeParams,
stay()
}
case Event(SpawnChannelNonInitiator(open, channelConfig, channelType, localParams, localFundingAmount_opt, peerConnection), d: ConnectedData) =>
case Event(SpawnChannelNonInitiator(open, channelConfig, channelType, addFunding_opt, localParams, peerConnection), d: ConnectedData) =>
val temporaryChannelId = open.fold(_.temporaryChannelId, _.temporaryChannelId)
if (peerConnection == d.peerConnection) {
val channel = spawnChannel()
@ -210,7 +210,7 @@ class Peer(val nodeParams: NodeParams,
channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, None, dualFunded = false, None, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType)
channel ! open
case Right(open) =>
channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, localFundingAmount_opt, dualFunded = true, None, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType)
channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, addFunding_opt, dualFunded = true, None, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType)
channel ! open
}
stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
@ -516,6 +516,7 @@ object Peer {
pushAmount_opt: Option[MilliSatoshi],
fundingTxFeerate_opt: Option[FeeratePerKw],
fundingTxFeeBudget_opt: Option[Satoshi],
requestFunding_opt: Option[LiquidityAds.RequestFunding],
channelFlags_opt: Option[ChannelFlags],
timeout_opt: Option[Timeout],
requireConfirmedInputsOverride_opt: Option[Boolean] = None,
@ -546,7 +547,7 @@ object Peer {
}
case class SpawnChannelInitiator(replyTo: akka.actor.typed.ActorRef[OpenChannelResponse], cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams)
case class SpawnChannelNonInitiator(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams, localFundingAmount_opt: Option[Satoshi], peerConnection: ActorRef)
case class SpawnChannelNonInitiator(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], channelConfig: ChannelConfig, channelType: SupportedChannelType, addFunding_opt: Option[LiquidityAds.AddFunding], localParams: LocalParams, peerConnection: ActorRef)
case class GetPeerInfo(replyTo: Option[typed.ActorRef[PeerInfoResponse]])
sealed trait PeerInfoResponse { def nodeId: PublicKey }

View file

@ -103,14 +103,20 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
}
when(BEFORE_INIT) {
case Event(InitializeConnection(peer, chainHash, localFeatures, doSync), d: BeforeInitData) =>
case Event(InitializeConnection(peer, chainHash, localFeatures, doSync, fundingRates_opt), d: BeforeInitData) =>
d.transport ! TransportHandler.Listener(self)
Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment()
log.debug(s"using features=$localFeatures")
val localInit = d.pendingAuth.address match {
case remoteAddress if !d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress)))
case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil)))
val remoteAddress_opt = d.pendingAuth.address match {
case remoteAddress if !d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(remoteAddress) => Some(InitTlv.RemoteAddress(remoteAddress))
case _ => None
}
val tlvs = TlvStream(Set(
Some(InitTlv.Networks(chainHash :: Nil)),
remoteAddress_opt,
fundingRates_opt.map(InitTlv.OptionWillFund)
).flatten[InitTlv])
val localInit = protocol.Init(localFeatures, tlvs)
d.transport ! localInit
startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout)
unstashAll() // unstash remote init if it already arrived
@ -574,7 +580,7 @@ object PeerConnection {
def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance
}
case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey, outgoing: Boolean) extends RemoteTypes
case class InitializeConnection(peer: ActorRef, chainHash: BlockHash, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes
case class InitializeConnection(peer: ActorRef, chainHash: BlockHash, features: Features[InitFeature], doSync: Boolean, fundingRates_opt: Option[LiquidityAds.WillFundRates]) extends RemoteTypes
case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: NodeAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes
sealed trait ConnectionResult extends RemoteTypes

View file

@ -102,7 +102,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
val hasChannels = peersWithChannels.contains(authenticated.remoteNodeId)
// if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && hasChannels)
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync, nodeParams.willFundRates_opt)
if (!hasChannels && !authenticated.outgoing) {
incomingConnectionsTracker ! TrackIncomingConnection(authenticated.remoteNodeId)
}

View file

@ -151,7 +151,8 @@ object EclairInternalsSerializer {
("peer" | actorRefCodec(system)) ::
("chainHash" | blockHash) ::
("features" | variableSizeBytes(uint16, initFeaturesCodec)) ::
("doSync" | bool(8))).as[PeerConnection.InitializeConnection]
("doSync" | bool(8)) ::
("fundingRates" | optional(bool(8), LiquidityAds.Codecs.willFundRates))).as[PeerConnection.InitializeConnection]
def connectionReadyCodec(system: ExtendedActorSystem): Codec[PeerConnection.ConnectionReady] = (
("peerConnection" | actorRefCodec(system)) ::

View file

@ -68,7 +68,7 @@ object Announcements {
)
}
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = {
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now(), fundingRates_opt: Option[LiquidityAds.WillFundRates] = None): NodeAnnouncement = {
require(alias.length <= 32)
// sort addresses by ascending address descriptor type; do not reorder addresses within the same descriptor type
val sortedAddresses = nodeAddresses.map {
@ -78,7 +78,8 @@ object Announcements {
case address@(_: Tor3) => (4, address)
case address@(_: DnsHostname) => (5, address)
}.sortBy(_._1).map(_._2)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty)
val tlvs = TlvStream(Set(fundingRates_opt.map(NodeAnnouncementTlv.OptionWillFund)).flatten[NodeAnnouncementTlv])
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, tlvs)
val sig = Crypto.sign(witness, nodeSecret)
NodeAnnouncement(
signature = sig,
@ -87,7 +88,8 @@ object Announcements {
rgbColor = color,
alias = alias,
features = features.unscoped(),
addresses = sortedAddresses
addresses = sortedAddresses,
tlvStream = tlvs
)
}

View file

@ -99,7 +99,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm
// on restart we update our node announcement
// note that if we don't currently have public channels, this will be ignored
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.willFundRates_opt)
self ! nodeAnn
log.info("initialization completed, ready to process messages")

View file

@ -209,7 +209,7 @@ object Validation {
// in case this was our first local channel, we make a node announcement
if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) {
log.info("first local channel validated, announcing local node")
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.willFundRates_opt)
handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn)
} else d1
}

View file

@ -64,6 +64,16 @@ object ChannelTlv {
val requireConfirmedInputsCodec: Codec[RequireConfirmedInputsTlv] = tlvField(provide(RequireConfirmedInputsTlv()))
/** Request inbound liquidity from our peer. */
case class RequestFundingTlv(request: LiquidityAds.RequestFunding) extends OpenDualFundedChannelTlv with TxInitRbfTlv with SpliceInitTlv
val requestFundingCodec: Codec[RequestFundingTlv] = tlvField(LiquidityAds.Codecs.requestFunding)
/** Accept inbound liquidity request. */
case class ProvideFundingTlv(willFund: LiquidityAds.WillFund) extends AcceptDualFundedChannelTlv with TxAckRbfTlv with SpliceAckTlv
val provideFundingCodec: Codec[ProvideFundingTlv] = tlvField(LiquidityAds.Codecs.willFund)
case class PushAmountTlv(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv
val pushAmountCodec: Codec[PushAmountTlv] = tlvField(tmillisatoshi)
@ -99,6 +109,8 @@ object OpenDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), requestFundingCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)
}
@ -119,6 +131,8 @@ object TxInitRbfTlv {
val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint)
.typecase(UInt64(0), tlvField(satoshiSigned.as[SharedOutputContributionTlv]))
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), requestFundingCodec)
)
}
@ -130,6 +144,8 @@ object TxAckRbfTlv {
val txAckRbfTlvCodec: Codec[TlvStream[TxAckRbfTlv]] = tlvStream(discriminated[TxAckRbfTlv].by(varint)
.typecase(UInt64(0), tlvField(satoshiSigned.as[SharedOutputContributionTlv]))
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), provideFundingCodec)
)
}
@ -139,6 +155,8 @@ object SpliceInitTlv {
val spliceInitTlvCodec: Codec[TlvStream[SpliceInitTlv]] = tlvStream(discriminated[SpliceInitTlv].by(varint)
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), requestFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
)
}
@ -149,6 +167,8 @@ object SpliceAckTlv {
val spliceAckTlvCodec: Codec[TlvStream[SpliceAckTlv]] = tlvStream(discriminated[SpliceAckTlv].by(varint)
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), provideFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
)
}
@ -165,6 +185,8 @@ object AcceptDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), provideFundingCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)

View file

@ -71,6 +71,7 @@ object CommonCodecs {
// this is needed because some millisatoshi values are encoded on 32 bits in the BOLTs
// this codec will fail if the amount does not fit on 32 bits
val millisatoshi32: Codec[MilliSatoshi] = uint32.xmapc(l => MilliSatoshi(l))(_.toLong)
val satoshi32: Codec[Satoshi] = uint32.xmapc(l => Satoshi(l))(_.toLong)
val timestampSecond: Codec[TimestampSecond] = uint32.xmapc(TimestampSecond(_))(_.toLong)

View file

@ -58,6 +58,7 @@ sealed trait HtlcFailureMessage extends HtlcSettlementMessage // <- not in the s
case class Init(features: Features[InitFeature], tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil)
val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address)
val fundingRates_opt = tlvStream.get[InitTlv.OptionWillFund].map(_.rates)
}
case class Warning(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[WarningTlv] = TlvStream.empty) extends SetupMessage with HasChannelId {
@ -134,29 +135,32 @@ case class TxInitRbf(channelId: ByteVector32,
tlvStream: TlvStream[TxInitRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId {
val fundingContribution: Satoshi = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount).getOrElse(0 sat)
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val requestFunding_opt: Option[LiquidityAds.RequestFunding] = tlvStream.get[ChannelTlv.RequestFundingTlv].map(_.request)
}
object TxInitRbf {
def apply(channelId: ByteVector32, lockTime: Long, feerate: FeeratePerKw, fundingContribution: Satoshi, requireConfirmedInputs: Boolean): TxInitRbf = {
def apply(channelId: ByteVector32, lockTime: Long, feerate: FeeratePerKw, fundingContribution: Satoshi, requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding]): TxInitRbf = {
val tlvs: Set[TxInitRbfTlv] = Set(
Some(TxRbfTlv.SharedOutputContributionTlv(fundingContribution)),
if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
requestFunding_opt.map(ChannelTlv.RequestFundingTlv)
).flatten
TxInitRbf(channelId, lockTime, feerate, TlvStream(tlvs))
}
}
case class TxAckRbf(channelId: ByteVector32,
tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId {
case class TxAckRbf(channelId: ByteVector32, tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId {
val fundingContribution: Satoshi = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount).getOrElse(0 sat)
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val willFund_opt: Option[LiquidityAds.WillFund] = tlvStream.get[ChannelTlv.ProvideFundingTlv].map(_.willFund)
}
object TxAckRbf {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, requireConfirmedInputs: Boolean): TxAckRbf = {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, requireConfirmedInputs: Boolean, willFund_opt: Option[LiquidityAds.WillFund]): TxAckRbf = {
val tlvs: Set[TxAckRbfTlv] = Set(
Some(TxRbfTlv.SharedOutputContributionTlv(fundingContribution)),
if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
willFund_opt.map(ChannelTlv.ProvideFundingTlv)
).flatten
TxAckRbf(channelId, TlvStream(tlvs))
}
@ -247,6 +251,7 @@ case class OpenDualFundedChannel(chainHash: BlockHash,
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script)
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val requestFunding_opt: Option[LiquidityAds.RequestFunding] = tlvStream.get[ChannelTlv.RequestFundingTlv].map(_.request)
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
}
@ -270,6 +275,7 @@ case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32,
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script)
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val willFund_opt: Option[LiquidityAds.WillFund] = tlvStream.get[ChannelTlv.ProvideFundingTlv].map(_.willFund)
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
}
@ -298,14 +304,16 @@ case class SpliceInit(channelId: ByteVector32,
fundingPubKey: PublicKey,
tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val requestFunding_opt: Option[LiquidityAds.RequestFunding] = tlvStream.get[ChannelTlv.RequestFundingTlv].map(_.request)
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
}
object SpliceInit {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean): SpliceInit = {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding]): SpliceInit = {
val tlvs: Set[SpliceInitTlv] = Set(
Some(ChannelTlv.PushAmountTlv(pushAmount)),
if (pushAmount > 0.msat) Some(ChannelTlv.PushAmountTlv(pushAmount)) else None,
if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
requestFunding_opt.map(ChannelTlv.RequestFundingTlv)
).flatten
SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, TlvStream(tlvs))
}
@ -316,14 +324,16 @@ case class SpliceAck(channelId: ByteVector32,
fundingPubKey: PublicKey,
tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
val willFund_opt: Option[LiquidityAds.WillFund] = tlvStream.get[ChannelTlv.ProvideFundingTlv].map(_.willFund)
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
}
object SpliceAck {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean): SpliceAck = {
def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, willFund_opt: Option[LiquidityAds.WillFund]): SpliceAck = {
val tlvs: Set[SpliceAckTlv] = Set(
Some(ChannelTlv.PushAmountTlv(pushAmount)),
if (pushAmount > 0.msat) Some(ChannelTlv.PushAmountTlv(pushAmount)) else None,
if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
willFund_opt.map(ChannelTlv.ProvideFundingTlv)
).flatten
SpliceAck(channelId, fundingContribution, fundingPubKey, TlvStream(tlvs))
}
@ -490,14 +500,13 @@ case class NodeAnnouncement(signature: ByteVector64,
alias: String,
addresses: List[NodeAddress],
tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp {
val fundingRates_opt = tlvStream.get[NodeAnnouncementTlv.OptionWillFund].map(_.rates)
val validAddresses: List[NodeAddress] = {
// if port is equal to 0, SHOULD ignore ipv6_addr OR ipv4_addr OR hostname; SHOULD ignore Tor v2 onion services.
val validAddresses = addresses.filter(address => address.port != 0 || address.isInstanceOf[Tor3]).filterNot(address => address.isInstanceOf[Tor2])
// if more than one type 5 address is announced, SHOULD ignore the additional data.
validAddresses.filter(!_.isInstanceOf[DnsHostname]) ++ validAddresses.find(_.isInstanceOf[DnsHostname])
}
val shouldRebroadcast: Boolean = {
// if more than one type 5 address is announced, MUST not forward the node_announcement.
addresses.count(address => address.isInstanceOf[DnsHostname]) <= 1

View file

@ -0,0 +1,271 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.wire.protocol
import com.google.common.base.Charsets
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel._
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvField
import fr.acinq.eclair.{ToMilliSatoshiConversion, UInt64}
import scodec.Codec
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
/**
* Created by t-bast on 12/04/2024.
*/
/**
* Liquidity ads create a decentralized market for channel liquidity.
* Nodes advertise funding rates for their available liquidity using the gossip protocol.
* Other nodes can then pay the advertised rate to get inbound liquidity allocated towards them.
*/
object LiquidityAds {
/**
* @param miningFee we refund the liquidity provider for some of the fee they paid to miners for the underlying on-chain transaction.
* @param serviceFee fee paid to the liquidity provider for the inbound liquidity.
*/
case class Fees(miningFee: Satoshi, serviceFee: Satoshi) {
val total: Satoshi = miningFee + serviceFee
}
/**
* Rate at which a liquidity seller sells its liquidity.
* Liquidity fees are computed based on multiple components.
*
* @param minAmount minimum amount that can be purchased at this rate.
* @param maxAmount maximum amount that can be purchased at this rate.
* @param fundingWeight the seller will have to add inputs/outputs to the transaction and pay on-chain fees
* for them. The buyer refunds those on-chain fees for the given vbytes.
* @param feeProportional proportional fee (expressed in basis points) based on the amount contributed by the seller.
* @param feeBase flat fee that must be paid regardless of the amount contributed by the seller.
* @param channelCreationFee flat fee that must be paid when a new channel is created.
*/
case class FundingRate(minAmount: Satoshi, maxAmount: Satoshi, fundingWeight: Int, feeProportional: Int, feeBase: Satoshi, channelCreationFee: Satoshi) {
/** Fees paid by the liquidity buyer. */
def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi, isChannelCreation: Boolean): Fees = {
val onChainFees = Transactions.weight2fee(feerate, fundingWeight)
// If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity.
val proportionalFee = requestedAmount.min(contributedAmount).toMilliSatoshi * feeProportional / 10_000
val flatFee = if (isChannelCreation) channelCreationFee + feeBase else feeBase
Fees(onChainFees, flatFee + proportionalFee.truncateToSatoshi)
}
/** Return true if this rate is compatible with the requested funding amount. */
def isCompatible(requestedAmount: Satoshi): Boolean = minAmount <= requestedAmount && requestedAmount <= maxAmount
/** When liquidity is purchased, the seller provides a signature of the funding rate and funding script. */
def signedData(fundingScript: ByteVector): ByteVector32 = {
// We use a tagged hash to ensure that our signature cannot be reused in a different context.
val tag = "liquidity_ads_purchase"
val fundingRateBin = Codecs.fundingRate.encode(this).require.bytes
Crypto.sha256(ByteVector(tag.getBytes(Charsets.US_ASCII)) ++ fundingRateBin ++ fundingScript)
}
}
/** The fees associated with a given [[FundingRate]] can be paid using various options. */
sealed trait PaymentType {
// @formatter:off
def rfcName: String
override def toString: String = rfcName
// @formatter:on
}
object PaymentType {
// @formatter:off
/** Fees are transferred from the buyer's channel balance to the seller's during the interactive-tx construction. */
case object FromChannelBalance extends PaymentType { override val rfcName: String = "from_channel_balance" }
/** Sellers may support unknown payment types, which we must ignore. */
case class Unknown(bitIndex: Int) extends PaymentType { override val rfcName: String = s"unknown_$bitIndex" }
// @formatter:on
}
/** When purchasing liquidity, we provide payment details matching one of the [[PaymentType]]s supported by the seller. */
sealed trait PaymentDetails extends Tlv {
def paymentType: PaymentType
}
object PaymentDetails {
// @formatter:off
case object FromChannelBalance extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromChannelBalance }
// @formatter:on
}
/** Sellers offer various rates and payment options. */
case class WillFundRates(fundingRates: List[FundingRate], paymentTypes: Set[PaymentType]) {
def validateRequest(nodeKey: PrivateKey, channelId: ByteVector32, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, request: RequestFunding, isChannelCreation: Boolean): Either[ChannelException, WillFundPurchase] = {
if (!paymentTypes.contains(request.paymentDetails.paymentType)) {
Left(InvalidLiquidityAdsPaymentType(channelId, request.paymentDetails.paymentType, paymentTypes))
} else if (!fundingRates.contains(request.fundingRate)) {
Left(InvalidLiquidityAdsRate(channelId))
} else if (!request.fundingRate.isCompatible(request.requestedAmount)) {
Left(InvalidLiquidityAdsRate(channelId))
} else {
val sig = Crypto.sign(request.fundingRate.signedData(fundingScript), nodeKey)
val purchase = Purchase.Standard(request.requestedAmount, request.fundingRate.fees(fundingFeerate, request.requestedAmount, request.requestedAmount, isChannelCreation), request.paymentDetails)
Right(WillFundPurchase(WillFund(request.fundingRate, fundingScript, sig), purchase))
}
}
def findRate(requestedAmount: Satoshi): Option[FundingRate] = fundingRates.find(r => r.minAmount <= requestedAmount && requestedAmount <= r.maxAmount)
}
def validateRequest(nodeKey: PrivateKey, channelId: ByteVector32, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, isChannelCreation: Boolean, request_opt: Option[RequestFunding], rates_opt: Option[WillFundRates]): Either[ChannelException, Option[WillFundPurchase]] = {
(request_opt, rates_opt) match {
case (Some(request), Some(rates)) => rates.validateRequest(nodeKey, channelId, fundingScript, fundingFeerate, request, isChannelCreation).map(l => Some(l))
case _ => Right(None)
}
}
/**
* Add funds to a channel when we're not the funding initiator.
*
* @param fundingAmount amount to add.
* @param rates_opt if provided, liquidity rates applied to our [[fundingAmount]] (otherwise we fund for free).
*/
case class AddFunding(fundingAmount: Satoshi, rates_opt: Option[WillFundRates])
/** Provide inbound liquidity to a remote peer that wants to purchase liquidity. */
case class WillFund(fundingRate: FundingRate, fundingScript: ByteVector, signature: ByteVector64)
/** Request inbound liquidity from a remote peer that supports liquidity ads. */
case class RequestFunding(requestedAmount: Satoshi, fundingRate: FundingRate, paymentDetails: PaymentDetails) {
def fees(fundingFeerate: FeeratePerKw, isChannelCreation: Boolean): Fees = fundingRate.fees(fundingFeerate, requestedAmount, requestedAmount, isChannelCreation)
def validateRemoteFunding(remoteNodeId: PublicKey,
channelId: ByteVector32,
fundingScript: ByteVector,
remoteFundingAmount: Satoshi,
fundingFeerate: FeeratePerKw,
isChannelCreation: Boolean,
willFund_opt: Option[WillFund]): Either[ChannelException, Purchase] = {
willFund_opt match {
case Some(willFund) =>
if (!Crypto.verifySignature(fundingRate.signedData(fundingScript), willFund.signature, remoteNodeId)) {
Left(InvalidLiquidityAdsSig(channelId))
} else if (remoteFundingAmount < requestedAmount) {
Left(InvalidLiquidityAdsAmount(channelId, remoteFundingAmount, requestedAmount))
} else if (willFund.fundingRate != fundingRate) {
Left(InvalidLiquidityAdsRate(channelId))
} else {
val purchasedAmount = requestedAmount.min(remoteFundingAmount)
val fees = fundingRate.fees(fundingFeerate, requestedAmount, remoteFundingAmount, isChannelCreation)
Right(Purchase.Standard(purchasedAmount, fees, paymentDetails))
}
case None =>
// If the remote peer doesn't want to provide inbound liquidity, we immediately fail the attempt.
// The user should retry this funding attempt without requesting inbound liquidity.
Left(MissingLiquidityAds(channelId))
}
}
}
def validateRemoteFunding(request_opt: Option[RequestFunding],
remoteNodeId: PublicKey,
channelId: ByteVector32,
fundingScript: ByteVector,
remoteFundingAmount: Satoshi,
fundingFeerate: FeeratePerKw,
isChannelCreation: Boolean,
willFund_opt: Option[WillFund]): Either[ChannelException, Option[Purchase]] = {
request_opt match {
case Some(request) => request.validateRemoteFunding(remoteNodeId, channelId, fundingScript, remoteFundingAmount, fundingFeerate, isChannelCreation, willFund_opt) match {
case Left(f) => Left(f)
case Right(purchase) => Right(Some(purchase))
}
case None => Right(None)
}
}
def requestFunding(amount: Satoshi, paymentDetails: PaymentDetails, remoteFundingRates: WillFundRates): Option[RequestFunding] = {
remoteFundingRates.findRate(amount) match {
case Some(fundingRate) if remoteFundingRates.paymentTypes.contains(paymentDetails.paymentType) => Some(RequestFunding(amount, fundingRate, paymentDetails))
case _ => None
}
}
/** Once a liquidity ads has been purchased, we keep track of the fees paid and the payment details. */
sealed trait Purchase {
// @formatter:off
def amount: Satoshi
def fees: Fees
def paymentDetails: PaymentDetails
// @formatter:on
}
object Purchase {
case class Standard(amount: Satoshi, fees: Fees, paymentDetails: PaymentDetails) extends Purchase()
}
case class WillFundPurchase(willFund: WillFund, purchase: Purchase)
object Codecs {
val fundingRate: Codec[FundingRate] = (
("minAmount" | satoshi32) ::
("maxAmount" | satoshi32) ::
("fundingWeight" | uint16) ::
("feeBasis" | uint16) ::
("feeBase" | satoshi32) ::
("channelCreationFee" | satoshi32)
).as[FundingRate]
private val paymentDetails: Codec[PaymentDetails] = discriminated[PaymentDetails].by(varint)
.typecase(UInt64(0), tlvField(provide(PaymentDetails.FromChannelBalance)))
val requestFunding: Codec[RequestFunding] = (
("requestedAmount" | satoshi) ::
("fundingRate" | fundingRate) ::
("paymentDetails" | paymentDetails)
).as[RequestFunding]
val willFund: Codec[WillFund] = (
("fundingRate" | fundingRate) ::
("fundingScript" | variableSizeBytes(uint16, bytes)) ::
("signature" | bytes64)
).as[WillFund]
private val paymentTypes: Codec[Set[PaymentType]] = bytes.xmap(
f = { bytes =>
bytes.bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, 0) => PaymentType.FromChannelBalance
case (true, idx) => PaymentType.Unknown(idx)
}.toSet
},
g = { paymentTypes =>
val indexes = paymentTypes.collect {
case PaymentType.FromChannelBalance => 0
case PaymentType.Unknown(idx) => idx
}
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits.
var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits
indexes.foreach { i => buf = buf.set(i) }
buf.reverse.bytes
}
)
val willFundRates: Codec[WillFundRates] = (
("fundingRates" | listOfN(uint16, fundingRate)) ::
("paymentTypes" | variableSizeBytes(uint16, paymentTypes))
).as[WillFundRates]
}
}

View file

@ -35,7 +35,13 @@ object AnnouncementSignaturesTlv {
sealed trait NodeAnnouncementTlv extends Tlv
object NodeAnnouncementTlv {
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint))
/** Rates at which the announced node sells inbound liquidity to remote peers. */
case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends NodeAnnouncementTlv
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), tlvField(LiquidityAds.Codecs.willFundRates.as[OptionWillFund]))
)
}
sealed trait ChannelAnnouncementTlv extends Tlv

View file

@ -41,6 +41,9 @@ object InitTlv {
*/
case class RemoteAddress(address: NodeAddress) extends InitTlv
/** Rates at which the sending node sells inbound liquidity to remote peers. */
case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends InitTlv
}
object InitTlvCodecs {
@ -49,10 +52,13 @@ object InitTlvCodecs {
private val networks: Codec[Networks] = tlvField(list(blockHash))
private val remoteAddress: Codec[RemoteAddress] = tlvField(nodeaddress)
private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.willFundRates)
val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint)
.typecase(UInt64(1), networks)
.typecase(UInt64(3), remoteAddress)
// We use a temporary TLV while the spec is being reviewed.
.typecase(UInt64(1339), willFund)
)
}

View file

@ -98,6 +98,11 @@ object TlvCodecs {
/** Truncated satoshi (0 to 8 bytes unsigned). */
val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong)
/**
* Truncated satoshi (0 to 4 bytes unsigned).
*/
val tsatoshi32: Codec[Satoshi] = tu32.xmap(l => Satoshi(l), s => s.toLong)
private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = {
if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) {
Attempt.Failure(Err("unknown even tlv type"))

View file

@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, Re
import fr.acinq.eclair.router.Graph.{MessagePath, WeightRatios}
import fr.acinq.eclair.router.PathFindingExperimentConf
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress, OnionRoutingPacket}
import fr.acinq.eclair.wire.protocol._
import org.scalatest.Tag
import scodec.bits.{ByteVector, HexStringSyntax}
@ -52,6 +52,10 @@ object TestConstants {
val nonInitiatorPushAmount: MilliSatoshi = 100_000_000L msat
val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat)
val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat)
val defaultLiquidityRates: LiquidityAds.WillFundRates = LiquidityAds.WillFundRates(
fundingRates = LiquidityAds.FundingRate(100_000 sat, 10_000_000 sat, 500, 100, 100 sat, 1000 sat) :: Nil,
paymentTypes = Set(LiquidityAds.PaymentType.FromChannelBalance)
)
val emptyOnionPacket: OnionRoutingPacket = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes)
val emptyOrigin = Origin.Hot(ActorRef.noSender, Upstream.Local(UUID.randomUUID()))
@ -232,6 +236,7 @@ object TestConstants {
),
purgeInvoicesInterval = None,
revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis),
willFundRates_opt = Some(defaultLiquidityRates),
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(enabled = false, timeout = 30 seconds),
)
@ -403,6 +408,7 @@ object TestConstants {
),
purgeInvoicesInterval = None,
revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis),
willFundRates_opt = Some(defaultLiquidityRates),
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(enabled = false, timeout = 30 seconds),
)

View file

@ -34,6 +34,7 @@ sealed trait TestDatabases extends Databases {
override def peers: PeersDb = db.peers
override def payments: PaymentsDb = db.payments
override def pendingCommands: PendingCommandsDb = db.pendingCommands
override def liquidity: LiquidityDb = db.liquidity
def close(): Unit
// @formatter:on
}

View file

@ -77,7 +77,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
aliceRegister ! alice
bobRegister ! bob
// no announcements
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, Alice.channelParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard())
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -23,7 +23,7 @@ import akka.testkit.TestProbe
import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt}
import fr.acinq.bitcoin.psbt.Psbt
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, OP_1, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId, TxOut, addressToPublicKeyScript}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, OP_1, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId, TxIn, TxOut, addressToPublicKeyScript}
import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, ProcessPsbtResponse}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
@ -34,8 +34,8 @@ import fr.acinq.eclair.blockchain.{OnChainWallet, SingleKeyOnChainWallet}
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
import fr.acinq.eclair.io.OpenChannelInterceptor.makeChannelParams
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey}
import org.scalatest.BeforeAndAfterAll
@ -122,60 +122,60 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
copy(fundingParamsA = fundingParamsA, fundingParamsB = fundingParamsB)
}
def spawnTxBuilderAlice(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsA): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
def spawnTxBuilderAlice(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsA, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsA, fundingParams, channelParamsA,
FundingTx(commitFeerate, firstPerCommitmentPointB, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, liquidityPurchase_opt,
wallet))
def spawnTxBuilderRbfAlice(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsA, fundingParams, channelParamsA,
PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, None,
wallet))
def spawnTxBuilderSpliceAlice(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
def spawnTxBuilderSpliceAlice(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsA, fundingParams, channelParamsA,
SpliceTx(commitment),
0 msat, 0 msat,
SpliceTx(commitment, CommitmentChanges.init()),
0 msat, 0 msat, liquidityPurchase_opt,
wallet))
def spawnTxBuilderSpliceRbfAlice(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsA, fundingParams, channelParamsA,
PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, None,
wallet))
def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsB, fundingParams, channelParamsB,
FundingTx(commitFeerate, firstPerCommitmentPointA, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, liquidityPurchase_opt,
wallet))
def spawnTxBuilderRbfBob(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsB, fundingParams, channelParamsB,
PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, None,
wallet))
def spawnTxBuilderSpliceBob(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
def spawnTxBuilderSpliceBob(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsB, fundingParams, channelParamsB,
SpliceTx(commitment),
0 msat, 0 msat,
SpliceTx(commitment, CommitmentChanges.init()),
0 msat, 0 msat, liquidityPurchase_opt,
wallet))
def spawnTxBuilderSpliceRbfBob(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder(
ByteVector32.Zeroes,
nodeParamsB, fundingParams, channelParamsB,
PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None),
0 msat, 0 msat,
0 msat, 0 msat, None,
wallet))
def exchangeSigsAliceFirst(fundingParams: InteractiveTxParams, successA: InteractiveTxBuilder.Succeeded, successB: InteractiveTxBuilder.Succeeded): (FullySignedSharedTransaction, Commitment, FullySignedSharedTransaction, Commitment) = {
@ -276,7 +276,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
}
}
private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs)(testFun: Fixture => Any): Unit = {
private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = {
// Initialize wallets with a few confirmed utxos.
val probe = TestProbe()
val rpcClientA = createWallet(UUID.randomUUID().toString)
@ -288,8 +288,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
generateBlocks(1)
val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs)
val alice = fixtureParams.spawnTxBuilderAlice(walletA)
val bob = fixtureParams.spawnTxBuilderBob(walletB)
val alice = fixtureParams.spawnTxBuilderAlice(walletA, liquidityPurchase_opt = liquidityPurchase_opt)
val bob = fixtureParams.spawnTxBuilderBob(walletB, liquidityPurchase_opt = liquidityPurchase_opt)
testFun(Fixture(alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe(), TestProbe()))
}
@ -569,7 +569,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
// We chose those amounts to ensure that Bob always signs first:
// - funding tx: Alice has one 380 000 sat input and Bob has one 350 000 sat input
// - splice tx: Alice has the shared input (150 000 sat) and one 380 000 sat input, Bob has one 350 000 sat input
// It verifies that we don't split the shared input amount: if we did,
// It verifies that we don't split the shared input amount: if we did, Alice would sign first.
val fundingA1 = 50_000 sat
val utxosA = Seq(380_000 sat, 380_000 sat)
val fundingB1 = 100_000 sat
@ -1616,7 +1616,97 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
}
}
test("funding splice transaction with previous inputs (different balance)") {
test("fund splice transaction from non-initiator without change output") {
val targetFeerate = FeeratePerKw(10_000 sat)
val fundingA = 100_000 sat
val utxosA = Seq(150_000 sat)
val fundingB = 92_000 sat
val utxosB = Seq(50_000 sat, 50_000 sat, 50_000 sat, 50_000 sat)
withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f =>
import f._
val probe = TestProbe()
alice ! Start(alice2bob.ref)
bob ! Start(bob2alice.ref)
// Alice --- tx_add_input --> Bob
fwd.forwardAlice2Bob[TxAddInput]
// Alice <-- tx_add_input --- Bob
fwd.forwardBob2Alice[TxAddInput]
// Alice --- tx_add_output --> Bob
fwd.forwardAlice2Bob[TxAddOutput]
// Alice <-- tx_add_input --- Bob
fwd.forwardBob2Alice[TxAddInput]
// Alice --- tx_add_output --> Bob
fwd.forwardAlice2Bob[TxAddOutput]
// Alice <-- tx_complete --- Bob
fwd.forwardBob2Alice[TxComplete]
// Alice --- tx_complete --> Bob
fwd.forwardAlice2Bob[TxComplete]
val successA1 = alice2bob.expectMsgType[Succeeded]
val successB1 = bob2alice.expectMsgType[Succeeded]
val (txA1, commitmentA1, _, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1)
assert(targetFeerate * 0.9 <= txA1.feerate && txA1.feerate <= targetFeerate * 1.25)
walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref)
probe.expectMsg(txA1.txId)
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[ChannelLiquidityPurchased])
// Alice initiates a splice that is only funded by Bob, because she is purchasing liquidity.
val purchase = LiquidityAds.Purchase.Standard(50_000 sat, LiquidityAds.Fees(1000 sat, 1500 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
// Alice pays fees for the common fields of the transaction, by decreasing her balance in the shared output.
val spliceFeeA = {
val dummySpliceTx = Transaction(
version = 2,
txIn = Seq(TxIn(commitmentA1.commitInput.outPoint, ByteVector.empty, 0, Scripts.witness2of2(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey))),
txOut = Seq(commitmentA1.commitInput.txOut),
lockTime = 0
)
Transactions.weight2fee(targetFeerate, dummySpliceTx.weight())
}
val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1)
val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -spliceFeeA, fundingAmountB = fundingB, targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = Nil, spliceOutputsB = Nil, requireConfirmedInputs = aliceParams.requireConfirmedInputs)
val fundingParamsA1 = spliceFixtureParams.fundingParamsA
val fundingParamsB1 = spliceFixtureParams.fundingParamsB
val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(fundingParamsA1, commitmentA1, walletA, liquidityPurchase_opt = Some(purchase))
val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(fundingParamsB1, commitmentB1, walletB, liquidityPurchase_opt = Some(purchase))
val fwdSplice = TypeCheckedForwarder(aliceSplice, bobSplice, alice2bob, bob2alice)
aliceSplice ! Start(alice2bob.ref)
bobSplice ! Start(bob2alice.ref)
// Alice --- tx_add_input --> Bob
fwdSplice.forwardAlice2Bob[TxAddInput]
// Alice <-- tx_add_input --- Bob
fwdSplice.forwardBob2Alice[TxAddInput]
// Alice --- tx_add_output --> Bob
fwdSplice.forwardAlice2Bob[TxAddOutput]
// Alice <-- tx_add_input --- Bob
fwdSplice.forwardBob2Alice[TxAddInput]
// Alice --- tx_complete --> Bob
fwdSplice.forwardAlice2Bob[TxComplete]
// Alice <-- tx_complete --- Bob
fwdSplice.forwardBob2Alice[TxComplete]
val successA2 = alice2bob.expectMsgType[Succeeded]
val successB2 = bob2alice.expectMsgType[Succeeded]
val (spliceTxA1, commitmentA2, _, commitmentB2) = fixtureParams.exchangeSigsBobFirst(fundingParamsB1, successA2, successB2)
assert(commitmentA2.localCommit.spec.toLocal == commitmentA1.localCommit.spec.toLocal - spliceFeeA - purchase.fees.total)
assert(commitmentB2.localCommit.spec.toLocal == commitmentB1.localCommit.spec.toLocal + fundingB + purchase.fees.total)
assert(targetFeerate * 0.9 <= spliceTxA1.feerate && spliceTxA1.feerate <= targetFeerate * 1.25)
walletA.publishTransaction(spliceTxA1.signedTx).pipeTo(probe.ref)
probe.expectMsg(spliceTxA1.txId)
val event = eventListener.expectMsgType[ChannelLiquidityPurchased]
assert(event.purchase.fees == purchase.fees)
assert(event.purchase.fundingTxIndex == 1)
assert(event.purchase.fundingTxId == spliceTxA1.txId)
}
}
test("fund splice transaction with previous inputs (different balance)") {
val targetFeerate = FeeratePerKw(2_500 sat)
val fundingA1 = 100_000 sat
val utxosA = Seq(340_000 sat, 40_000 sat, 35_000 sat)
@ -2095,6 +2185,24 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
}
}
test("invalid funding contributions for liquidity purchase") {
val probe = TestProbe()
val wallet = new SingleKeyOnChainWallet()
val purchase = LiquidityAds.Purchase.Standard(500_000 sat, LiquidityAds.Fees(5000 sat, 20_000 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
val params = createFixtureParams(24_000 sat, 500_000 sat, FeeratePerKw(5000 sat), 500 sat, 0)
// Bob will reject Alice's proposal, since she doesn't have enough funds to pay the liquidity fees.
val bob = params.spawnTxBuilderBob(wallet, params.fundingParamsB, Some(purchase))
bob ! Start(probe.ref)
assert(probe.expectMsgType[LocalFailure].cause == InvalidFundingBalances(params.channelId, 524_000 sat, 525_000_000 msat, -1_000_000 msat))
// Bob reject a splice proposed by Alice where she doesn't have enough funds to pay the liquidity fees.
val previousCommitment = CommitmentsSpec.makeCommitments(450_000_000 msat, 50_000_000 msat).active.head
val sharedInput = params.dummySharedInputB(500_000 sat)
val spliceParams = params.fundingParamsB.copy(localContribution = 150_000 sat, remoteContribution = -30_000 sat, sharedInput_opt = Some(sharedInput))
val bobSplice = params.spawnTxBuilderSpliceBob(spliceParams, previousCommitment, wallet, Some(purchase))
bobSplice ! Start(probe.ref)
assert(probe.expectMsgType[LocalFailure].cause == InvalidFundingBalances(params.channelId, 620_000 sat, 625_000_000 msat, -5_000_000 msat))
}
test("invalid input") {
val probe = TestProbe()
// Create a transaction with a mix of segwit and non-segwit inputs.

View file

@ -28,11 +28,11 @@ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet, OnchainPubkeyCache, SingleKeyOnChainWallet}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.send.SpontaneousRecipient
import fr.acinq.eclair.payment.{Invoice, OutgoingPaymentPacket}
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route}
@ -51,6 +51,8 @@ object ChannelStateTestsTags {
val DisableWumbo = "disable_wumbo"
/** If set, channels will use option_dual_fund. */
val DualFunding = "dual_funding"
/** If set, a liquidity ads will be used when opening a channel. */
val LiquidityAds = "liquidity_ads"
/** If set, peers will support splicing. */
val Splicing = "splicing"
/** If set, channels will use option_static_remotekey. */
@ -244,20 +246,31 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic))
val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags)
val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
val dualFunded = tags.contains(ChannelStateTestsTags.DualFunding)
val fundingAmount = TestConstants.fundingSatoshis
val initiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount)
val nonInitiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NonInitiatorPushAmount)) Some(TestConstants.nonInitiatorPushAmount) else None
val nonInitiatorFundingAmount = if (dualFunded) Some(TestConstants.nonInitiatorFundingSatoshis) else None
val dualFunded = tags.contains(ChannelStateTestsTags.DualFunding)
val liquidityAds = tags.contains(ChannelStateTestsTags.LiquidityAds)
val requestFunds_opt = if (liquidityAds) {
Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance))
} else {
None
}
val nonInitiatorFunding_opt = if (dualFunded) {
val leaseRates_opt = if (liquidityAds) Some(TestConstants.defaultLiquidityRates) else None
Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, leaseRates_opt))
} else {
None
}
val eventListener = TestProbe()
systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished])
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunds_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFundingAmount, dualFunded, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFunding_opt, dualFunded, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes)
val fundingTx = if (!dualFunded) {
@ -360,10 +373,6 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
eventually(assert(alice.stateName == NORMAL))
eventually(assert(bob.stateName == NORMAL))
val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments
val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments
val expectedBalanceBob = (nonInitiatorFundingAmount.getOrElse(0 sat) + initiatorPushAmount.getOrElse(0 msat) - nonInitiatorPushAmount.getOrElse(0 msat) - aliceCommitments.latest.remoteChannelReserve).max(0 msat)
assert(bobCommitments.availableBalanceForSend == expectedBalanceBob)
// x2 because alice and bob share the same relayer
channelUpdateListener.expectMsgType[LocalChannelUpdate]
channelUpdateListener.expectMsgType[LocalChannelUpdate]

View file

@ -66,7 +66,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
val fundingAmount = if (test.tags.contains(LargeChannel)) Btc(5).toSatoshi else TestConstants.fundingSatoshis
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -172,7 +172,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
// Bob advertises support for anchor outputs, but Alice doesn't.
val aliceParams = Alice.channelParams
val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional))
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs())
val open = alice2bob.expectMsgType[OpenChannel]
assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs()))

View file

@ -18,6 +18,7 @@ package fr.acinq.eclair.channel.states.a
import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps
import akka.testkit.{TestFSMRef, TestProbe}
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong}
import fr.acinq.eclair.TestConstants.Alice
import fr.acinq.eclair.channel._
@ -25,8 +26,8 @@ import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel}
import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass}
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, LiquidityAds, OpenDualFundedChannel}
import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes64}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -35,7 +36,6 @@ import scala.concurrent.duration.DurationInt
class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase {
val bobRequiresConfirmedInputs = "bob_requires_confirmed_inputs"
val dualFundingContribution = "dual_funding_contribution"
case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], open: OpenDualFundedChannel, aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, listener: TestProbe)
@ -54,12 +54,17 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
val nonInitiatorContribution = if (test.tags.contains(dualFundingContribution)) Some(TestConstants.nonInitiatorFundingSatoshis) else None
val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) else None
val requestFunds_opt = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) {
Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance))
} else {
None
}
val nonInitiatorPushAmount = if (test.tags.contains(ChannelStateTestsTags.NonInitiatorPushAmount)) Some(TestConstants.nonInitiatorPushAmount) else None
val listener = TestProbe()
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunds_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
val open = alice2bob.expectMsgType[OpenDualFundedChannel]
alice2bob.forward(bob, open)
@ -87,19 +92,52 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
aliceOpenReplyTo.expectNoMessage()
}
test("recv AcceptDualFundedChannel (with non-initiator contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag(dualFundingContribution), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv AcceptDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
assert(accept.upfrontShutdownScript_opt.isEmpty)
assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))
assert(accept.fundingAmount == TestConstants.nonInitiatorFundingSatoshis)
assert(accept.willFund_opt.nonEmpty)
assert(accept.pushAmount == 0.msat)
bob2alice.forward(alice, accept)
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
}
test("recv AcceptDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(dualFundingContribution), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv AcceptDualFundedChannel (with invalid liquidity ads sig)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
val willFundInvalidSig = accept.willFund_opt.get.copy(signature = randomBytes64())
val acceptInvalidSig = accept
.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.ProvideFundingTlv]))
.modify(_.tlvStream.records).using(_ + ChannelTlv.ProvideFundingTlv(willFundInvalidSig))
bob2alice.forward(alice, acceptInvalidSig)
assert(alice2bob.expectMsgType[Error].toAscii.contains("liquidity ads signature is invalid"))
awaitCond(alice.stateName == CLOSED)
}
test("recv AcceptDualFundedChannel (with invalid liquidity ads amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel].copy(fundingAmount = TestConstants.nonInitiatorFundingSatoshis / 2)
bob2alice.forward(alice, accept)
assert(alice2bob.expectMsgType[Error].toAscii.contains("liquidity ads funding amount is too low"))
awaitCond(alice.stateName == CLOSED)
}
test("recv AcceptDualFundedChannel (without liquidity ads response)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
val acceptMissingWillFund = accept.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.ProvideFundingTlv]))
bob2alice.forward(alice, acceptMissingWillFund)
assert(alice2bob.expectMsgType[Error].toAscii.contains("liquidity ads field is missing"))
awaitCond(alice.stateName == CLOSED)
}
test("recv AcceptDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
@ -111,7 +149,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
}
test("recv AcceptDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(dualFundingContribution), Tag(bobRequiresConfirmedInputs), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv AcceptDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(bobRequiresConfirmedInputs), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
@ -122,7 +160,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
}
test("recv AcceptDualFundedChannel (negative funding amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(dualFundingContribution), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv AcceptDualFundedChannel (negative funding amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
@ -133,7 +171,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt
aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected]
}
test("recv AcceptDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(dualFundingContribution), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv AcceptDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]

View file

@ -57,7 +57,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
val listener = TestProbe()
within(30 seconds) {
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain, listener)))

View file

@ -23,7 +23,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, OpenDualFundedChannel}
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, LiquidityAds, OpenDualFundedChannel}
import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -52,13 +52,14 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur
val channelConfig = ChannelConfig.standard
val channelFlags = ChannelFlags(announceChannel = false)
val pushAmount = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount)
val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) else None
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
val requireConfirmedInputs = test.tags.contains(aliceRequiresConfirmedInputs)
within(30 seconds) {
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushAmount, requireConfirmedInputs, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushAmount, requireConfirmedInputs, None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
awaitCond(bob.stateName == WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, aliceListener, bobListener)))
}
@ -95,6 +96,18 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED)
}
test("recv OpenDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val open = alice2bob.expectMsgType[OpenDualFundedChannel]
val requestFunds = LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val openWithFundsRequest = open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records + ChannelTlv.RequestFundingTlv(requestFunds)))
alice2bob.forward(bob, openWithFundsRequest)
val accept = bob2alice.expectMsgType[AcceptDualFundedChannel]
assert(accept.fundingAmount == TestConstants.nonInitiatorFundingSatoshis)
assert(accept.willFund_opt.nonEmpty)
}
test("recv OpenDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._

View file

@ -27,7 +27,7 @@ import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout
import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelReestablish, CommitSig, Error, Init, OpenDualFundedChannel, TxAbort, TxAckRbf, TxAddInput, TxAddOutput, TxComplete, TxInitRbf, Warning}
import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelReestablish, CommitSig, Error, Init, LiquidityAds, OpenDualFundedChannel, TxAbort, TxAckRbf, TxAddInput, TxAddOutput, TxComplete, TxInitRbf, Warning}
import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -53,8 +53,8 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted])
bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id
bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id
alice2bob.expectMsgType[OpenDualFundedChannel]

View file

@ -50,7 +50,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
val bobContribution = if (channelType.features.contains(Features.ZeroConf)) None else Some(TestConstants.nonInitiatorFundingSatoshis)
val bobContribution = if (channelType.features.contains(Features.ZeroConf)) None else Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None))
val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None)
val commitFeerate = channelType.commitmentFormat match {
case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw
@ -61,7 +61,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted])
bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id
bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id

View file

@ -65,7 +65,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
val listener = TestProbe()
within(30 seconds) {
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -52,7 +52,7 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu
val listener = TestProbe()
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)

View file

@ -68,7 +68,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
val listener = TestProbe()
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -65,7 +65,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu
alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted])
bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted])
alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushMsat, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushMsat, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -21,6 +21,7 @@ import akka.testkit.{TestFSMRef, TestProbe}
import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.blockchain.{CurrentBlockHeight, SingleKeyOnChainWallet}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
@ -32,7 +33,7 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFacto
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -40,6 +41,9 @@ import scala.concurrent.duration.DurationInt
class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase {
val bothPushAmount = "both_push_amount"
val noFundingContribution = "no_funding_contribution"
case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceListener: TestProbe, bobListener: TestProbe, wallet: SingleKeyOnChainWallet)
override def withFixture(test: OneArgTest): Outcome = {
@ -69,10 +73,16 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
}
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
val bobContribution = if (test.tags.contains("no-funding-contribution")) None else Some(TestConstants.nonInitiatorFundingSatoshis)
val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None)
val (requestFunds_opt, bobContribution) = if (test.tags.contains(noFundingContribution)) {
(None, None)
} else {
val requestFunds = LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val addFunding = LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))
(Some(requestFunds), Some(addFunding))
}
val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains(bothPushAmount)) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None)
within(30 seconds) {
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunds_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2blockchain.expectMsgType[SetChannelId] // temporary channel id
bob2blockchain.expectMsgType[SetChannelId] // temporary channel id
@ -123,6 +133,13 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid)
assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid)
}
if (!test.tags.contains(noFundingContribution)) {
// Alice pays fees for the liquidity she bought, and push amounts are correctly transferred.
val liquidityFees = TestConstants.defaultLiquidityRates.fundingRates.head.fees(TestConstants.feeratePerKw, TestConstants.nonInitiatorFundingSatoshis, TestConstants.nonInitiatorFundingSatoshis, isChannelCreation = true)
val bobReserve = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.remoteChannelReserve
val expectedBalanceBob = bobContribution.map(_.fundingAmount).getOrElse(0 sat) + liquidityFees.total + initiatorPushAmount.getOrElse(0 msat) - nonInitiatorPushAmount.getOrElse(0 msat) - bobReserve
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.availableBalanceForSend == expectedBalanceBob)
}
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceListener, bobListener, wallet)))
}
}
@ -241,7 +258,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val probe = TestProbe()
val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None)
alice2bob.expectMsgType[TxInitRbf]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAckRbf]
@ -308,16 +325,18 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
assert(bob2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.fundingTxId == fundingTx1.txid)
}
def testBumpFundingFees(f: FixtureParam): FullySignedSharedTransaction = {
def testBumpFundingFees(f: FixtureParam, feerate_opt: Option[FeeratePerKw] = None, requestFunding_opt: Option[LiquidityAds.RequestFunding] = None): FullySignedSharedTransaction = {
import f._
val probe = TestProbe()
val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, feerate_opt.getOrElse(currentFundingTx.feerate * 1.1), fundingFeeBudget = 100_000.sat, 0, requestFunding_opt)
assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis)
val txAckRbf = bob2alice.expectMsgType[TxAckRbf]
assert(txAckRbf.fundingContribution == TestConstants.nonInitiatorFundingSatoshis)
requestFunding_opt.foreach(_ => assert(txAckRbf.willFund_opt.nonEmpty))
bob2alice.forward(alice)
// Alice and Bob build a new version of the funding transaction, with one new input every time.
@ -366,9 +385,23 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
test("recv CMD_BUMP_FUNDING_FEE", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._
val remoteFunding = TestConstants.nonInitiatorFundingSatoshis
val feerate1 = TestConstants.feeratePerKw
val liquidityFee1 = TestConstants.defaultLiquidityRates.fundingRates.head.fees(feerate1, remoteFunding, remoteFunding, isChannelCreation = true)
val balanceBob1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty)
testBumpFundingFees(f)
testBumpFundingFees(f)
val feerate2 = FeeratePerKw(12_500 sat)
val liquidityFee2 = TestConstants.defaultLiquidityRates.fundingRates.head.fees(feerate2, remoteFunding, remoteFunding, isChannelCreation = true)
testBumpFundingFees(f, Some(feerate2), Some(LiquidityAds.RequestFunding(remoteFunding, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)))
val balanceBob2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal
assert(liquidityFee1.total < liquidityFee2.total)
assert(balanceBob1 + liquidityFee2.total - liquidityFee1.total == balanceBob2)
// The second RBF attempt removes the liquidity request.
val feerate3 = FeeratePerKw(15_000 sat)
testBumpFundingFees(f, Some(feerate3), requestFunding_opt = None)
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal.truncateToSatoshi == remoteFunding)
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 2)
}
@ -378,7 +411,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val probe = TestProbe()
val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None)
assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis)
@ -411,7 +444,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val probe = TestProbe()
val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100.sat, 0, None)
assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis)
@ -437,7 +470,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
test("recv TxInitRbf (exhausted RBF attempts)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.RejectRbfAttempts)) { f =>
import f._
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, 500_000 sat, requireConfirmedInputs = false)
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, 500_000 sat, requireConfirmedInputs = false, None)
assert(bob2alice.expectMsgType[TxAbort].toAscii == InvalidRbfAttemptsExhausted(channelId(bob), 0).getMessage)
assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
@ -446,27 +479,27 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
import f._
val currentBlockHeight = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.createdAt
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, 500_000 sat, requireConfirmedInputs = false)
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, 500_000 sat, requireConfirmedInputs = false, None)
assert(bob2alice.expectMsgType[TxAbort].toAscii == InvalidRbfAttemptTooSoon(channelId(bob), currentBlockHeight, currentBlockHeight + 1).getMessage)
assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
test("recv TxInitRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f =>
test("recv TxInitRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(bothPushAmount)) { f =>
import f._
val fundingBelowPushAmount = 199_000.sat
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, fundingBelowPushAmount, requireConfirmedInputs = false)
bob ! TxInitRbf(channelId(bob), 0, TestConstants.feeratePerKw * 1.25, fundingBelowPushAmount, requireConfirmedInputs = false, None)
assert(bob2alice.expectMsgType[TxAbort].toAscii == InvalidPushAmount(channelId(bob), TestConstants.initiatorPushAmount, fundingBelowPushAmount.toMilliSatoshi).getMessage)
assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
test("recv TxAckRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f =>
test("recv TxAckRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(bothPushAmount)) { f =>
import f._
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, fundingFeeBudget = 100_000.sat, 0, None)
alice2bob.expectMsgType[TxInitRbf]
val fundingBelowPushAmount = 99_000.sat
alice ! TxAckRbf(channelId(alice), fundingBelowPushAmount, requireConfirmedInputs = false)
alice ! TxAckRbf(channelId(alice), fundingBelowPushAmount, requireConfirmedInputs = false, None)
assert(alice2bob.expectMsgType[TxAbort].toAscii == InvalidPushAmount(channelId(alice), TestConstants.nonInitiatorPushAmount, fundingBelowPushAmount.toMilliSatoshi).getMessage)
assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
}
@ -525,7 +558,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
awaitCond(alice.stateName == CLOSED)
}
test("recv CurrentBlockCount (funding timeout reached)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f =>
test("recv CurrentBlockCount (funding timeout reached)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f =>
import f._
val timeoutBlock = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + Channel.FUNDING_TIMEOUT_FUNDEE + 1
bob ! ProcessCurrentBlockHeight(CurrentBlockHeight(timeoutBlock))
@ -535,7 +568,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
awaitCond(bob.stateName == CLOSED)
}
test("recv CurrentBlockCount (funding timeout reached while offline)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f =>
test("recv CurrentBlockCount (funding timeout reached while offline)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f =>
import f._
val timeoutBlock = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + Channel.FUNDING_TIMEOUT_FUNDEE + 1
bob ! INPUT_DISCONNECTED
@ -561,7 +594,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY)
}
test("recv ChannelReady (initiator, no remote contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f =>
test("recv ChannelReady (initiator, no remote contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f =>
import f._
val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx
bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx)
@ -759,7 +792,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None)
alice2bob.expectMsgType[TxInitRbf]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAckRbf]
@ -818,7 +851,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
import f._
val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0)
alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None)
alice2bob.expectMsgType[TxInitRbf]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAckRbf]
@ -963,7 +996,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
assert(alice.stateName == CLOSING)
}
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f =>
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f =>
import f._
val commitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! Error(ByteVector32.Zeroes, "please help me recover my funds")

View file

@ -52,8 +52,8 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF
val listener = TestProbe()
within(30 seconds) {
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
alice2blockchain.expectMsgType[SetChannelId] // temporary channel id
bob2blockchain.expectMsgType[SetChannelId] // temporary channel id
alice2bob.expectMsgType[OpenDualFundedChannel]

View file

@ -61,7 +61,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted])
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed])
val commitTxFeerate = if (test.tags.contains(ChannelStateTestsTags.AnchorOutputs) || test.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, commitTxFeerate, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -97,7 +97,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
val sender = TestProbe()
val scriptPubKey = Script.write(Script.pay2wpkh(randomKey().publicKey))
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, scriptPubKey)))
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, scriptPubKey)), requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[Stfu]
if (!sendInitialStfu) {
@ -117,7 +117,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
import f._
// we have an unsigned htlc in our local changes
addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
alice ! CMD_SPLICE(TestProbe().ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None)
alice ! CMD_SPLICE(TestProbe().ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None)
alice2bob.expectNoMessage(100 millis)
crossSign(alice, bob, alice2bob, bob2alice)
alice2bob.expectMsgType[Stfu]
@ -390,7 +390,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[Stfu]
bob ! cmd
@ -407,7 +407,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectNoMessage(100 millis) // alice isn't quiescent yet
bob ! cmd
@ -421,6 +421,25 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
bob2alice.expectMsgType[SpliceInit]
}
test("initiate quiescence concurrently (pending changes on non-initiator side)") { f =>
import f._
addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[Stfu]
bob ! cmd
bob2alice.expectNoMessage(100 millis) // bob isn't quiescent yet
alice2bob.forward(bob)
crossSign(bob, alice, bob2alice, alice2bob)
bob2alice.expectMsgType[Stfu]
bob2alice.forward(alice)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NonInitiatorQuiescent)
sender.expectMsgType[RES_FAILURE[CMD_SPLICE, ConcurrentRemoteSplice]]
alice2bob.expectMsgType[SpliceInit]
}
test("outgoing htlc timeout during quiescence negotiation") { f =>
import f._
val (_, add) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)

View file

@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states.e
import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.testkit.{TestFSMRef, TestProbe}
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.scalacompat.NumericSatoshi.abs
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction, TxIn}
@ -33,7 +34,6 @@ import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId}
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM}
import fr.acinq.eclair.channel.states.ChannelStateTestsTags._
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.db.RevokedHtlcInfoCleaner.ForgetHtlcInfos
import fr.acinq.eclair.payment.relay.Relayer
@ -58,7 +58,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging
override def withFixture(test: OneArgTest): Outcome = {
val tags = test.tags + DualFunding + Splicing
val tags = test.tags + ChannelStateTestsTags.DualFunding + ChannelStateTestsTags.Splicing
val setup = init(tags = tags)
import setup._
reachNormal(setup, tags)
@ -77,7 +77,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): TestProbe = {
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None)
s ! cmd
if (useQuiescence(s)) {
exchangeStfu(s, r, s2r, r2s)
@ -276,10 +276,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
}
test("recv CMD_SPLICE (splice-in, non dual-funded channel)") { () =>
val f = init(tags = Set(DualFunding, Splicing))
val f = init(tags = Set(ChannelStateTestsTags.DualFunding, ChannelStateTestsTags.Splicing))
import f._
reachNormal(f, tags = Set(Splicing)) // we open a non dual-funded channel
reachNormal(f, tags = Set(ChannelStateTestsTags.Splicing)) // we open a non dual-funded channel
alice2bob.ignoreMsg { case _: ChannelUpdate => true }
bob2alice.ignoreMsg { case _: ChannelUpdate => true }
awaitCond(alice.stateName == NORMAL && bob.stateName == NORMAL)
@ -304,7 +304,126 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(postSpliceState.commitments.latest.remoteChannelReserve == 15_000.sat)
}
test("recv CMD_SPLICE (splice-in, local and remote commit index mismatch)", Tag(Quiescence)) { f =>
test("recv CMD_SPLICE (splice-in, liquidity ads)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val sender = TestProbe()
val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest))
alice ! cmd
exchangeStfu(alice, bob, alice2bob, bob2alice)
assert(alice2bob.expectMsgType[SpliceInit].requestFunding_opt.nonEmpty)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[SpliceAck].willFund_opt.nonEmpty)
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAddInput]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAddInput]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAddInput]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAddOutput]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAddOutput]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxComplete]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAddOutput]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxComplete]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxComplete]
alice2bob.forward(bob)
exchangeSpliceSigs(alice, bob, alice2bob, bob2alice, sender)
// Alice paid fees to Bob for the additional liquidity.
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.capacity == 2_400_000.sat)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal < 1_300_000_000.msat)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote > 1_100_000_000.msat)
}
test("recv CMD_SPLICE (splice-in, liquidity ads, invalid lease witness)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val sender = TestProbe()
val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest))
alice ! cmd
exchangeStfu(alice, bob, alice2bob, bob2alice)
assert(alice2bob.expectMsgType[SpliceInit].requestFunding_opt.nonEmpty)
alice2bob.forward(bob)
val spliceAck = bob2alice.expectMsgType[SpliceAck]
assert(spliceAck.willFund_opt.nonEmpty)
val spliceAckInvalidWitness = spliceAck
.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.ProvideFundingTlv]))
.modify(_.tlvStream.records).using(_ + ChannelTlv.ProvideFundingTlv(spliceAck.willFund_opt.get.copy(signature = randomBytes64())))
bob2alice.forward(alice, spliceAckInvalidWitness)
alice2bob.expectMsgType[TxAbort]
alice2bob.forward(bob)
bob2alice.expectMsgType[TxAbort]
bob2alice.forward(alice)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.capacity == 1_500_000.sat)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 800_000_000.msat)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat)
}
test("recv CMD_SPLICE (splice-in, liquidity ads, below minimum funding amount)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val sender = TestProbe()
val fundingRequest = LiquidityAds.RequestFunding(5_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest))
alice ! cmd
exchangeStfu(alice, bob, alice2bob, bob2alice)
assert(alice2bob.expectMsgType[SpliceInit].requestFunding_opt.nonEmpty)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("liquidity ads funding rates don't match"))
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAbort]
alice2bob.forward(bob)
}
test("recv CMD_SPLICE (splice-in, liquidity ads, invalid funding rate)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val sender = TestProbe()
val fundingRequest = LiquidityAds.RequestFunding(100_000 sat, LiquidityAds.FundingRate(10_000 sat, 200_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest))
alice ! cmd
exchangeStfu(alice, bob, alice2bob, bob2alice)
assert(alice2bob.expectMsgType[SpliceInit].requestFunding_opt.nonEmpty)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("liquidity ads funding rates don't match"))
bob2alice.forward(alice)
alice2bob.expectMsgType[TxAbort]
alice2bob.forward(bob)
}
test("recv CMD_SPLICE (splice-in, liquidity ads, cannot pay fees)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
// Alice requests a lot of funding, but she doesn't have enough balance to pay the corresponding fee.
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 800_000_000.msat)
val fundingRequest = LiquidityAds.RequestFunding(5_000_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)
val cmd = CMD_SPLICE(sender.ref, None, Some(SpliceOut(750_000 sat, defaultSpliceOutScriptPubKey)), Some(fundingRequest))
alice ! cmd
exchangeStfu(alice, bob, alice2bob, bob2alice)
assert(alice2bob.expectMsgType[SpliceInit].requestFunding_opt.nonEmpty)
alice2bob.forward(bob)
assert(bob2alice.expectMsgType[SpliceAck].willFund_opt.nonEmpty)
bob2alice.forward(alice)
assert(alice2bob.expectMsgType[TxAbort].toAscii.contains("invalid balances"))
assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("invalid balances"))
}
test("recv CMD_SPLICE (splice-in, local and remote commit index mismatch)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
// Alice and Bob asynchronously exchange HTLCs, which makes their commit indices diverge.
@ -374,18 +493,18 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
setupHtlcs(f)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(780_000.sat, defaultSpliceOutScriptPubKey)))
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(780_000.sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None)
alice ! cmd
sender.expectMsgType[RES_FAILURE[_, _]]
}
test("recv CMD_SPLICE (splice-out, would go below reserve, quiescent)", Tag(Quiescence), Tag(NoMaxHtlcValueInFlight)) { f =>
test("recv CMD_SPLICE (splice-out, would go below reserve, quiescent)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
setupHtlcs(f)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)))
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None)
alice ! cmd
exchangeStfu(f)
sender.expectMsgType[RES_FAILURE[_, _]]
@ -395,7 +514,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
// we tweak the feerate
val spliceInit = alice2bob.expectMsgType[SpliceInit].copy(feerate = FeeratePerKw(100.sat))
@ -415,7 +534,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
val sender = TestProbe()
val bobBalance = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None)
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None, requestFunding_opt = None)
val spliceInit = alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob, spliceInit)
val spliceAck = bob2alice.expectMsgType[SpliceAck]
@ -472,7 +591,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testSpliceInAndOutCmd(f)
}
test("recv CMD_SPLICE (splice-in + splice-out, quiescence)", Tag(Quiescence)) { f =>
test("recv CMD_SPLICE (splice-in + splice-out, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testSpliceInAndOutCmd(f)
}
@ -480,7 +599,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
import f._
val sender = TestProbe()
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)))
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None)
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
bob2alice.expectMsgType[SpliceAck]
@ -504,7 +623,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
import f._
val sender = TestProbe()
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)))
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None)
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
bob2alice.expectMsgType[SpliceAck]
@ -539,7 +658,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
import f._
val sender = TestProbe()
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None)
alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None)
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
bob2alice.expectMsgType[SpliceAck]
@ -583,7 +702,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
}
test("recv WatchFundingConfirmedTriggered on splice tx", Tag(NoMaxHtlcValueInFlight)) { f =>
test("recv WatchFundingConfirmedTriggered on splice tx", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
@ -650,7 +769,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
(fundingTx1, fundingTx2)
}
test("splice local/remote locking", Tag(NoMaxHtlcValueInFlight)) { f =>
test("splice local/remote locking", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val (fundingTx1, fundingTx2) = setup2Splices(f)
@ -684,7 +803,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.map(_.fundingTxIndex) == Seq.empty)
}
test("splice local/remote locking (zero-conf)", Tag(NoMaxHtlcValueInFlight), Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("splice local/remote locking (zero-conf)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(250_000 sat)))
@ -708,7 +827,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.fundingTxId == fundingTx1.txid)
}
test("splice local/remote locking (reverse order)", Tag(NoMaxHtlcValueInFlight)) { f =>
test("splice local/remote locking (reverse order)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val (fundingTx1, fundingTx2) = setup2Splices(f)
@ -738,7 +857,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.map(_.fundingTxIndex) == Seq.empty)
}
test("splice local/remote locking (intermingled)", Tag(NoMaxHtlcValueInFlight)) { f =>
test("splice local/remote locking (intermingled)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val (fundingTx1, fundingTx2) = setup2Splices(f)
@ -772,7 +891,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.map(_.fundingTxIndex) == Seq.empty)
}
test("emit post-splice events", Tag(NoMaxHtlcValueInFlight), Tag(Quiescence)) { f =>
test("emit post-splice events", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight), Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
// Alice and Bob asynchronously exchange HTLCs, which makes their commit indices diverge.
@ -900,7 +1019,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
test("recv CMD_ADD_HTLC while a splice is requested") { f =>
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[SpliceInit]
alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, localOrigin(sender.ref))
@ -911,7 +1030,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
test("recv CMD_ADD_HTLC while a splice is in progress") { f =>
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
@ -926,7 +1045,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
test("recv UpdateAddHtlc while a splice is requested") { f =>
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[SpliceInit]
// we're holding the splice_init to create a race
@ -951,7 +1070,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
test("recv UpdateAddHtlc while a splice is in progress") { f =>
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
@ -968,7 +1087,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(!alice.stateData.asInstanceOf[DATA_NORMAL].commitments.hasPendingOrProposedHtlcs)
}
test("recv UpdateAddHtlc before splice confirms (zero-conf)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv UpdateAddHtlc before splice confirms (zero-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val spliceTx = initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)))
@ -992,7 +1111,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.head.localCommit.spec.htlcs.size == 1)
}
test("recv UpdateAddHtlc while splice is being locked", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv UpdateAddHtlc while splice is being locked", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)))
@ -1089,7 +1208,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
import f._
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None)
alice ! cmd
alice2bob.expectMsgType[SpliceInit]
alice2bob.forward(bob)
@ -1137,7 +1256,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testDisconnectCommitSigNotReceived(f)
}
test("disconnect (commit_sig not received, quiescence)", Tag(Quiescence)) { f =>
test("disconnect (commit_sig not received, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testDisconnectCommitSigNotReceived(f)
}
@ -1177,7 +1296,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testDisconnectCommitSigReceivedByAlice(f)
}
test("disconnect (commit_sig received by alice, quiescence)", Tag(Quiescence)) { f =>
test("disconnect (commit_sig received by alice, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testDisconnectCommitSigReceivedByAlice(f)
}
@ -1219,7 +1338,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testDisconnectTxSignaturesSentByBob(f)
}
test("disconnect (tx_signatures sent by bob, quiescence)", Tag(Quiescence)) { f =>
test("disconnect (tx_signatures sent by bob, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testDisconnectTxSignaturesSentByBob(f)
}
@ -1268,7 +1387,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testDisconnectTxSignaturesReceivedByAlice(f)
}
test("disconnect (tx_signatures received by alice, quiescence)", Tag(Quiescence)) { f =>
test("disconnect (tx_signatures received by alice, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testDisconnectTxSignaturesReceivedByAlice(f)
}
@ -1314,11 +1433,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs, spliceOutFee = 0.sat)
}
test("disconnect (tx_signatures received by alice, zero-conf)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("disconnect (tx_signatures received by alice, zero-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testDisconnectTxSignaturesReceivedByAliceZeroConf(f)
}
test("disconnect (tx_signatures received by alice, zero-conf, quiescence)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs), Tag(Quiescence)) { f =>
test("disconnect (tx_signatures received by alice, zero-conf, quiescence)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.Quiescence)) { f =>
testDisconnectTxSignaturesReceivedByAliceZeroConf(f)
}
@ -1354,7 +1473,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
}
test("don't resend splice_locked when zero-conf channel confirms", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("don't resend splice_locked when zero-conf channel confirms", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)))
@ -1551,7 +1670,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testForceCloseWithMultipleSplicesSimple(f)
}
test("force-close with multiple splices (simple, quiescence)", Tag(Quiescence)) { f =>
test("force-close with multiple splices (simple, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
testForceCloseWithMultipleSplicesSimple(f)
}
@ -1637,7 +1756,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testForceCloseWithMultipleSplicesPreviousActiveRemote(f)
}
test("force-close with multiple splices (previous active remote, quiescence)", Tag(Quiescence), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (previous active remote, quiescence)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesPreviousActiveRemote(f)
}
@ -1717,7 +1836,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
testForceCloseWithMultipleSplicesPreviousActiveRevoked(f)
}
test("force-close with multiple splices (previous active revoked, quiescent)", Tag(Quiescence), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (previous active revoked, quiescent)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesPreviousActiveRevoked(f)
}
@ -1828,11 +1947,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose]))
}
test("force-close with multiple splices (inactive remote)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (inactive remote)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesInactiveRemote(f)
}
test("force-close with multiple splices (inactive remote, quiescence)", Tag(Quiescence), Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (inactive remote, quiescence)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesInactiveRemote(f)
}
@ -1947,11 +2066,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose]))
}
test("force-close with multiple splices (inactive revoked)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (inactive revoked)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesInactiveRevoked(f)
}
test("force-close with multiple splices (inactive revoked, quiescence)", Tag(Quiescence), Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("force-close with multiple splices (inactive revoked, quiescence)", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
testForceCloseWithMultipleSplicesInactiveRevoked(f)
}
@ -1990,7 +2109,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2blockchain.expectNoMessage(100 millis)
}
test("put back watches after restart (inactive)", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("put back watches after restart (inactive)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val fundingTx0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
@ -2049,7 +2168,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2blockchain.expectNoMessage(100 millis)
}
test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs", Tag(Quiescence)) { f =>
test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val htlcs = setupHtlcs(f)
@ -2086,7 +2205,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs, spliceOutFee = 0.sat)
}
test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked", Tag(Quiescence), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f =>
test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val htlcs = setupHtlcs(f)
@ -2105,7 +2224,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs, spliceOutFee = 0.sat)
}
test("recv multiple CMD_SPLICE (splice-in, splice-out, quiescence)", Tag(Quiescence)) { f =>
test("recv multiple CMD_SPLICE (splice-in, splice-out, quiescence)", Tag(ChannelStateTestsTags.Quiescence)) { f =>
val htlcs = setupHtlcs(f)
initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
@ -2114,7 +2233,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs, spliceOutFee = spliceOutFee(f, capacity = 1_900_000.sat))
}
test("recv invalid htlc signatures during splice-in", Tag(Quiescence)) { f =>
test("recv invalid htlc signatures during splice-in", Tag(ChannelStateTestsTags.Quiescence)) { f =>
import f._
val htlcs = setupHtlcs(f)

View file

@ -75,7 +75,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped)
alice2blockchain.expectMsgType[SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[SetChannelId]

View file

@ -0,0 +1,60 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db
import fr.acinq.bitcoin.scalacompat.{SatoshiLong, TxId}
import fr.acinq.eclair.TestDatabases.forAllDbs
import fr.acinq.eclair.channel.{ChannelLiquidityPurchased, LiquidityPurchase}
import fr.acinq.eclair.wire.protocol.LiquidityAds
import fr.acinq.eclair.{MilliSatoshiLong, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuite
class LiquidityDbSpec extends AnyFunSuite {
test("add/list liquidity purchases") {
forAllDbs { dbs =>
val db = dbs.liquidity
val (nodeId1, nodeId2) = (randomKey().publicKey, randomKey().publicKey)
val confirmedFundingTxId = TxId(randomBytes32())
val unconfirmedFundingTxId = TxId(randomBytes32())
val e1a = ChannelLiquidityPurchased(null, randomBytes32(), nodeId1, LiquidityPurchase(confirmedFundingTxId, 3, isBuyer = true, 250_000 sat, LiquidityAds.Fees(2_000 sat, 3_000 sat), 750_000 sat, 50_000 sat, 300_000 sat, 400_000_000 msat, 350_000_000 msat, 7, 11))
val e1b = ChannelLiquidityPurchased(null, randomBytes32(), nodeId1, LiquidityPurchase(confirmedFundingTxId, 7, isBuyer = false, 50_000 sat, LiquidityAds.Fees(300 sat, 700 sat), 500_000 sat, 50_000 sat, 0 sat, 250_000_000 msat, 250_000_000 msat, 0, 0))
val e1c = ChannelLiquidityPurchased(null, e1b.channelId, nodeId1, LiquidityPurchase(confirmedFundingTxId, 0, isBuyer = false, 150_000 sat, LiquidityAds.Fees(500 sat, 1_500 sat), 250_000 sat, 150_000 sat, -100_000 sat, 200_000_000 msat, 50_000_000 msat, 47, 45))
val e1d = ChannelLiquidityPurchased(null, randomBytes32(), nodeId1, LiquidityPurchase(unconfirmedFundingTxId, 22, isBuyer = true, 250_000 sat, LiquidityAds.Fees(4_000 sat, 1_000 sat), 450_000 sat, -50_000 sat, 250_000 sat, 150_000_000 msat, 300_000_000 msat, 3, 3))
val e2a = ChannelLiquidityPurchased(null, randomBytes32(), nodeId2, LiquidityPurchase(confirmedFundingTxId, 453, isBuyer = false, 200_000 sat, LiquidityAds.Fees(1_000 sat, 1_000 sat), 300_000 sat, 250_000 sat, 0 sat, 270_000_000 msat, 30_000_000 msat, 113, 0))
val e2b = ChannelLiquidityPurchased(null, randomBytes32(), nodeId2, LiquidityPurchase(unconfirmedFundingTxId, 1, isBuyer = false, 200_000 sat, LiquidityAds.Fees(1_000 sat, 1_000 sat), 300_000 sat, 250_000 sat, -10_000 sat, 250_000_000 msat, 50_000_000 msat, 0, 113))
db.addPurchase(e1a)
db.addPurchase(e1b)
db.addPurchase(e1c)
db.addPurchase(e1d)
db.addPurchase(e2a)
db.addPurchase(e2b)
// The liquidity purchase is confirmed only once the corresponding transaction confirms.
assert(db.listPurchases(nodeId1).isEmpty)
assert(db.listPurchases(nodeId2).isEmpty)
db.setConfirmed(nodeId1, confirmedFundingTxId)
db.setConfirmed(nodeId2, confirmedFundingTxId)
assert(db.listPurchases(nodeId1).toSet == Set(e1a, e1b, e1c).map(_.purchase))
assert(db.listPurchases(nodeId2) == Seq(e2a.purchase))
}
}
}

View file

@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH
import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _}
import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass, randomBytes32}
import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass}
import grizzled.slf4j.Logging
import org.json4s.{DefaultFormats, Formats}
import org.scalatest.BeforeAndAfterAll
@ -184,6 +184,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit
pushAmount_opt = Some(pushMsat),
fundingTxFeerate_opt = None,
fundingTxFeeBudget_opt = None,
requestFunding_opt = None,
channelFlags_opt = None,
timeout_opt = None))
sender.expectMsgType[OpenChannelResponse.Created](10 seconds)

View file

@ -181,7 +181,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat
def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): OpenChannelResponse.Created = {
val sender = TestProbe("sender")
sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, channelType_opt, None, None, None, None, None))
sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, channelType_opt, None, None, None, None, None, None))
sender.expectMsgType[OpenChannelResponse.Created]
}

View file

@ -72,7 +72,7 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu
val aliceInit = Init(Alice.channelParams.initFeatures)
val bobInit = Init(Bob.channelParams.initFeatures)
// alice and bob will both have 1 000 000 sat
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, commitTxFeerate = feeratePerKw, fundingTxFeerate = feeratePerKw, fundingTxFeeBudget_opt = None, Some(1000000000 msat), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, ChannelFlags(announceChannel = false), channelConfig, channelType, replyTo = system.deadLetters)
alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, commitTxFeerate = feeratePerKw, fundingTxFeerate = feeratePerKw, fundingTxFeeBudget_opt = None, Some(1000000000 msat), requireConfirmedInputs = false, requestFunding_opt = None, Alice.channelParams, pipe, bobInit, ChannelFlags(announceChannel = false), channelConfig, channelType, replyTo = system.deadLetters)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, Bob.channelParams, pipe, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]

View file

@ -24,7 +24,7 @@ import com.softwaremill.quicklens.ModifyPimp
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ChannelType, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.blockchain.DummyOnChainWallet
import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType
import fr.acinq.eclair.channel.fsm.Channel
@ -34,7 +34,7 @@ import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelInit
import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelNonInitiator}
import fr.acinq.eclair.io.PeerSpec.createOpenChannelMessage
import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel
import fr.acinq.eclair.wire.protocol.{ChannelTlv, Error, IPAddress, NodeAddress, OpenChannel, OpenChannelTlv, TlvStream}
import fr.acinq.eclair.wire.protocol.{ChannelTlv, Error, IPAddress, LiquidityAds, NodeAddress, OpenChannel, OpenChannelTlv, TlvStream}
import fr.acinq.eclair.{AcceptOpenChannel, CltvExpiryDelta, FeatureSupport, Features, InitFeature, InterceptOpenChannelCommand, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, MilliSatoshiLong, RejectOpenChannel, TestConstants, UnknownFeature, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -92,14 +92,24 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress)
openChannelInterceptor ! openChannelNonInitiator
pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel
pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(50_000 sat))
val response = peer.expectMessageType[SpawnChannelNonInitiator]
assert(response.localFundingAmount_opt.contains(50_000 sat))
assert(response.localParams.dustLimit == defaultParams.dustLimit)
assert(response.localParams.htlcMinimum == defaultParams.htlcMinimum)
assert(response.localParams.maxAcceptedHtlcs == defaultParams.maxAcceptedHtlcs)
assert(response.localParams.maxHtlcValueInFlightMsat == defaultParams.maxHtlcValueInFlightMsat)
assert(response.localParams.toSelfDelay == defaultParams.toSelfDelay)
pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, addFunding_opt = None)
val updatedLocalParams = peer.expectMessageType[SpawnChannelNonInitiator].localParams
assert(updatedLocalParams.dustLimit == defaultParams.dustLimit)
assert(updatedLocalParams.htlcMinimum == defaultParams.htlcMinimum)
assert(updatedLocalParams.maxAcceptedHtlcs == defaultParams.maxAcceptedHtlcs)
assert(updatedLocalParams.maxHtlcValueInFlightMsat == defaultParams.maxHtlcValueInFlightMsat)
assert(updatedLocalParams.toSelfDelay == defaultParams.toSelfDelay)
}
test("add liquidity if interceptor plugin requests it") { f =>
import f._
val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress)
openChannelInterceptor ! openChannelNonInitiator
pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel
val addFunding = LiquidityAds.AddFunding(100_000 sat, None)
pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(addFunding))
assert(peer.expectMessageType[SpawnChannelNonInitiator].addFunding_opt.contains(addFunding))
}
test("continue channel open if no interceptor plugin registered and pending channels rate limiter accepts it") { f =>
@ -112,7 +122,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
openChannelInterceptor ! openChannelNonInitiator
pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel
pluginInterceptor.expectNoMessage(10 millis)
peer.expectMessageType[SpawnChannelNonInitiator]
assert(peer.expectMessageType[SpawnChannelNonInitiator].addFunding_opt.isEmpty)
}
test("reject open channel request if channel type is obsolete") { f =>
@ -181,7 +191,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), staticRemoteKeyFeatures, staticRemoteKeyFeatures, peerConnection.ref, remoteAddress)
openChannelInterceptor ! openChannelNonInitiator
pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel
pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(50_000 sat))
pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(LiquidityAds.AddFunding(50_000 sat, None)))
peer.expectMessageType[SpawnChannelNonInitiator]
}
@ -190,7 +200,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val probe = TestProbe[Any]()
val fundingAmountBig = Channel.MAX_FUNDING_WITHOUT_WUMBO + 10_000.sat
openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), defaultFeatures, defaultFeatures.add(Wumbo, Optional))
openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None, None), defaultFeatures, defaultFeatures.add(Wumbo, Optional))
assert(probe.expectMessageType[OpenChannelResponse.Rejected].reason.contains("you must enable large channels support"))
}
@ -199,7 +209,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val probe = TestProbe[Any]()
val fundingAmountBig = Channel.MAX_FUNDING_WITHOUT_WUMBO + 10_000.sat
openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), defaultFeatures.add(Wumbo, Optional), defaultFeatures)
openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None, None), defaultFeatures.add(Wumbo, Optional), defaultFeatures)
assert(probe.expectMessageType[OpenChannelResponse.Rejected].reason == s"fundingAmount=$fundingAmountBig is too big, the remote peer doesn't support wumbo")
}

View file

@ -74,7 +74,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = isPersistent))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId, outgoing = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync, None))
transport.expectMsgType[TransportHandler.Listener]
val localInit = transport.expectMsgType[protocol.Init]
assert(localInit.networks == List(Block.RegtestGenesisBlock.hash))
@ -102,7 +102,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.send(peerConnection, incomingConnection)
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId, outgoing = false))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false, None))
transport.expectMsgType[TransportHandler.Listener]
val localInit = transport.expectMsgType[protocol.Init]
assert(localInit.remoteAddress_opt == Some(fakeIPAddress))
@ -134,7 +134,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(peerConnection)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true, None))
probe.expectTerminated(peerConnection, nodeParams.peerConnectionConf.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out"))
peer.expectMsg(ConnectionDown(peerConnection))
@ -147,7 +147,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true, None))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value)
@ -164,7 +164,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true, None))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value)
@ -181,7 +181,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true, None))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
// remote activated MPP but forgot payment secret
@ -199,7 +199,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true, None))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, protocol.Init(Bob.nodeParams.features.initFeatures(), TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SignetGenesisBlock.hash :: Nil))))

View file

@ -363,9 +363,10 @@ class PeerSpec extends FixtureSpec {
connect(remoteNodeId, peer, peerConnection, switchboard)
assert(peer.stateData.channels.isEmpty)
val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, None, None)
val requestFunds = LiquidityAds.RequestFunding(50_000 sat, LiquidityAds.FundingRate(10_000 sat, 100_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, Some(requestFunds), None, None)
peerConnection.send(peer, open)
channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].requestFunding_opt.contains(requestFunds))
}
test("don't spawn a dual funded channel if not supported") { f =>
@ -384,7 +385,7 @@ class PeerSpec extends FixtureSpec {
// Both peers support option_dual_fund, so it is automatically used.
connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)))
assert(peer.stateData.channels.isEmpty)
probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None, None, None))
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded)
}
@ -427,15 +428,15 @@ class PeerSpec extends FixtureSpec {
connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory)))
assert(peer.stateData.channels.isEmpty)
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None))
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.StaticRemoteKey())
// We can create channels that don't use the features we have enabled.
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.Standard()), None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.Standard()), None, None, None, None, None, None))
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.Standard())
// We can create channels that use features that we haven't enabled.
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None, None))
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputs())
}
@ -470,7 +471,7 @@ class PeerSpec extends FixtureSpec {
// We ensure the current network feerate is higher than the default anchor output feerate.
nodeParams.setFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat)))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.channelType == ChannelTypes.AnchorOutputs())
assert(!init.dualFunded)
@ -488,7 +489,7 @@ class PeerSpec extends FixtureSpec {
// We ensure the current network feerate is higher than the default anchor output feerate.
nodeParams.setFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat)))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx())
assert(!init.dualFunded)
@ -502,7 +503,7 @@ class PeerSpec extends FixtureSpec {
val probe = TestProbe()
connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory)))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.channelType == ChannelTypes.StaticRemoteKey())
assert(!init.dualFunded)
@ -519,12 +520,12 @@ class PeerSpec extends FixtureSpec {
assert(peer.underlyingActor.nodeParams.channelConf.maxHtlcValueInFlightMsat == 100_000_000.msat)
{
probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, None, None, None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.localParams.maxHtlcValueInFlightMsat == 50_000_000.msat) // max-htlc-value-in-flight-percent
}
{
probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.localParams.maxHtlcValueInFlightMsat == 100_000_000.msat) // max-htlc-value-in-flight-msat
}
@ -548,7 +549,7 @@ class PeerSpec extends FixtureSpec {
import f._
intercept[IllegalArgumentException] {
Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, None, Some(ChannelFlags(announceChannel = true)), None)
Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, None, None, Some(ChannelFlags(announceChannel = true)), None)
}
}
@ -557,7 +558,7 @@ class PeerSpec extends FixtureSpec {
val probe = TestProbe()
connect(remoteNodeId, peer, peerConnection, switchboard)
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None, None))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR]
assert(init.replyTo == probe.ref.toTyped[OpenChannelResponse])
}
@ -631,7 +632,7 @@ class PeerSpec extends FixtureSpec {
val open = createOpenChannelMessage()
system.eventStream.subscribe(probe.ref, classOf[ChannelAborted])
connect(remoteNodeId, peer, peerConnection, switchboard)
peer ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, ChannelTypes.Standard(), localParams, None, ActorRef.noSender)
peer ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, ChannelTypes.Standard(), None, localParams, ActorRef.noSender)
val channelAborted = probe.expectMsgType[ChannelAborted]
assert(channelAborted.remoteNodeId == remoteNodeId)
assert(channelAborted.channelId == open.temporaryChannelId)

View file

@ -24,7 +24,7 @@ import fr.acinq.eclair._
import fr.acinq.eclair.router.Announcements._
import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.nodeAnnouncementCodec
import fr.acinq.eclair.wire.protocol.NodeAddress
import fr.acinq.eclair.wire.protocol.{NodeAddress, TlvStream}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._
@ -51,7 +51,7 @@ class AnnouncementsSpec extends AnyFunSuite {
val bitcoin_b_sig = Announcements.signChannelAnnouncement(witness, bitcoin_b)
val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig)
assert(checkSigs(ann))
assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) == false)
assert(!checkSigs(ann.copy(nodeId1 = randomKey().publicKey)))
}
test("create valid signed node announcement") {
@ -76,7 +76,10 @@ class AnnouncementsSpec extends AnyFunSuite {
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
))
assert(checkSig(ann))
assert(checkSig(ann.copy(timestamp = 153 unixsec)) == false)
assert(!checkSig(ann.copy(timestamp = 153 unixsec)))
val annLiquidityAds = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures(), fundingRates_opt = Some(TestConstants.defaultLiquidityRates))
assert(checkSig(annLiquidityAds))
assert(!checkSig(annLiquidityAds.copy(tlvStream = TlvStream.empty)))
}
test("sort node announcement addresses") {
@ -131,7 +134,7 @@ class AnnouncementsSpec extends AnyFunSuite {
test("create valid signed channel update announcement") {
val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.channelConf.expiryDelta, Alice.nodeParams.channelConf.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat)
assert(checkSig(ann, Alice.nodeParams.nodeId))
assert(checkSig(ann, randomKey().publicKey) == false)
assert(!checkSig(ann, randomKey().publicKey))
}
test("check flags") {

View file

@ -145,7 +145,7 @@ class ChannelCodecs4Spec extends AnyFunSuite {
)
val testCases = Map(
RbfStatus.NoRbf -> RbfStatus.NoRbf,
RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 100_000.sat, 0)) -> RbfStatus.NoRbf,
RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 100_000.sat, 0, None)) -> RbfStatus.NoRbf,
RbfStatus.RbfInProgress(None, null, None) -> RbfStatus.NoRbf,
RbfStatus.RbfWaitingForSigs(waitingForSigs) -> RbfStatus.RbfWaitingForSigs(waitingForSigs),
RbfStatus.RbfAborted -> RbfStatus.NoRbf,

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol
import com.google.common.base.Charsets
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, OutPoint, SatoshiLong, ScriptWitness, Transaction, TxId}
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, OutPoint, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId}
import fr.acinq.eclair.FeatureSupport.Optional
import fr.acinq.eclair.Features.DataLossProtect
import fr.acinq.eclair.TestUtils.randomTxId
@ -27,6 +27,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.json.JsonSerializers
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.protocol.ChannelTlv.{ChannelTypeTlv, PushAmountTlv, RequireConfirmedInputsTlv, UpfrontShutdownScriptTlv}
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._
@ -75,7 +76,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 031302b6438bb1c1f90556487c0acb2ba33cc22608", hex"088a", List(chainHash1), Some(remoteAddress2), valid = true), // single network and IPv6 address
TestCase(hex"0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202", hex"088a", List(chainHash1, chainHash2), None, valid = true), // multiple networks
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 c9012a", hex"088a", List(chainHash1), None, valid = true), // network and unknown odd records
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 02012a", hex"088a", Nil, None, valid = false) // network and unknown even records
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 02012a", hex"088a", Nil, None, valid = false), // network and unknown even records
TestCase(hex"0000 0002088a fd053b190001000186a00007a1200226006400001388000003e8000101", hex"088a", Nil, None, valid = true), // one liquidity ads with the default payment type
TestCase(hex"0000 0002088a fd053b470002000186a00007a1200226006400001388000003e80007a120004c4b40044c004b00000000000005dc001b080000000000000000000300000000000000000000000000000001", hex"088a", Nil, None, valid = true) // two liquidity ads with multiple payment types
)
for (testCase <- testCases) {
@ -179,6 +182,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
// This is random, longer mainnet transaction.
val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000"
val tx2 = Transaction.read(txBin2.toArray)
val fundingRate = LiquidityAds.FundingRate(25_000 sat, 250_000 sat, 750, 150, 50 sat, 500 sat)
val testCases = Seq(
TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005",
TxAddInput(channelId2, UInt64(0), Some(tx2), 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000",
@ -194,13 +198,15 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000",
TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), -25_000 sat, requireConfirmedInputs = false) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008ffffffffffff9e58",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), -25_000 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008ffffffffffff9e58",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(50_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000 fd053b1e000000000000c350000061a80003d09002ee009600000032000001f40000",
TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
TxAckRbf(channelId2, 450_000 sat, requireConfirmedInputs = false) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008000000000006ddd0",
TxAckRbf(channelId2, 0 sat, requireConfirmedInputs = false) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 00080000000000000000",
TxAckRbf(channelId2, -250_000 sat, requireConfirmedInputs = true) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008fffffffffffc2f70 0200",
TxAckRbf(channelId2, 450_000 sat, requireConfirmedInputs = false, None) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008000000000006ddd0",
TxAckRbf(channelId2, 0 sat, requireConfirmedInputs = false, None) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 00080000000000000000",
TxAckRbf(channelId2, -250_000 sat, requireConfirmedInputs = true, None) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008fffffffffffc2f70 0200",
TxAckRbf(channelId2, 50_000 sat, requireConfirmedInputs = true, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008000000000000c350 0200 fd053b5a000061a80003d09002ee009600000032000001f40004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000",
TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 000e 696e7465726e616c206572726f72",
)
@ -360,6 +366,34 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
}
}
test("encode/decode splice messages") {
val channelId = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
val fundingTxId = TxId(TxHash(ByteVector32(hex"24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566")))
val fundingPubkey = PublicKey(hex"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
val fundingRate = LiquidityAds.FundingRate(100_000.sat, 100_000.sat, 400, 150, 0.sat, 0.sat)
val testCases = Seq(
// @formatter:off
SpliceInit(channelId, 100_000 sat, FeeratePerKw(2500 sat), 100, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = false, None) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840",
SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000",
SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680",
SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceAck(channelId, (-25_000).sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566",
// @formatter:on
)
testCases.foreach { case (message, bin) =>
val decoded = lightningMessageCodec.decode(bin.bits).require.value
assert(decoded == message)
val encoded = lightningMessageCodec.encode(message).require.bytes
assert(encoded == bin)
}
}
test("encode/decode closing_signed") {
val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101")
val testCases = Seq(
@ -379,6 +413,57 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
}
}
test("encode/decode liquidity ads") {
val willFundRates = LiquidityAds.WillFundRates(
fundingRates = List(
LiquidityAds.FundingRate(100_000 sat, 500_000 sat, 550, 100, 5_000 sat, 1000 sat),
LiquidityAds.FundingRate(500_000 sat, 5_000_000 sat, 1100, 75, 0 sat, 1500 sat),
),
Set(LiquidityAds.PaymentType.FromChannelBalance)
)
val nodeKey = PrivateKey(hex"57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3")
val nodeAnn = Announcements.makeNodeAnnouncement(nodeKey, "LN-Liquidity", Color(42, 117, 87), Nil, Features.empty, TimestampSecond(1713171401), Some(willFundRates))
val nodeAnnCommonBin = hex"0101 22ec2e2a6e02f54d949e332cbce571d123ae20dda98d0340ac7e64f60f11d413659a2a9645adea8f886bb5dd40cc589bd3e0f4f8b2ab333d323b74b7762b4ca1 0000 661cebc9 03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413 2a7557 4c4e2d4c69717569646974790000000000000000000000000000000000000000 0000"
val fundingRateBin1 = hex"000186a0 0007a120 0226 0064 00001388 000003e8"
val fundingRateBin2 = hex"0007a120 004c4b40 044c 004b 00000000 000005dc"
// <length> <payment_types_bitfield>
val paymentTypesBin = hex"0001 01"
// <tag> <length> <rates_count> <rate1> <rate2> <payment_types>
val nodeAnnTlvsBin = hex"fd053b" ++ hex"2d" ++ hex"0002" ++ fundingRateBin1 ++ fundingRateBin2 ++ paymentTypesBin
assert(lightningMessageCodec.encode(nodeAnn).require.bytes == nodeAnnCommonBin ++ nodeAnnTlvsBin)
assert(lightningMessageCodec.decode((nodeAnnCommonBin ++ nodeAnnTlvsBin).bits).require.value == nodeAnn)
assert(Announcements.checkSig(nodeAnn))
val defaultOpen = OpenDualFundedChannel(Block.LivenetGenesisBlock.hash, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), publicKey(7), ChannelFlags(true))
val defaultOpenBin = hex"0040 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f 01"
assert(lightningMessageCodec.encode(defaultOpen).require.bytes == defaultOpenBin)
val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 700_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), point(7))
val defaultAcceptBin = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 00000000000aae60 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f"
assert(lightningMessageCodec.encode(defaultAccept).require.bytes == defaultAcceptBin)
val fundingScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(defaultOpen.fundingPubkey, defaultAccept.fundingPubkey)))
val Some(request) = LiquidityAds.requestFunding(750_000 sat, LiquidityAds.PaymentDetails.FromChannelBalance, willFundRates)
val open = defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(request)))
val openBin = hex"fd053b 1e 00000000000b71b0 0007a120004c4b40044c004b00000000000005dc 0000"
assert(lightningMessageCodec.encode(open).require.bytes == defaultOpenBin ++ openBin)
val Right(willFund) = willFundRates.validateRequest(nodeKey, randomBytes32(), fundingScript, defaultOpen.fundingFeerate, request, isChannelCreation = true).map(_.willFund)
val accept = defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund)))
val acceptBin = hex"fd053b 78 0007a120004c4b40044c004b00000000000005dc 002200202ec38203f4cf37a3b377d9a55c7ae0153c643046dbdbe2ffccfb11b74420103c c57cf393f6bd534472ec08cbfbbc7268501b32f563a21cdf02a99127c4f25168249acd6509f96b2e93843c3b838ee4808c75d0a15ff71ba886fda980b8ca954f"
assert(lightningMessageCodec.encode(accept).require.bytes == defaultAcceptBin ++ acceptBin)
}
test("decode unknown liquidity ads payment types") {
val fundingRate = LiquidityAds.FundingRate(100_000 sat, 500_000 sat, 550, 100, 5_000 sat, 0 sat)
val testCases = Map(
hex"0001 000186a00007a120022600640000138800000000 001b 080000000000000000000000000000000008000000000000000001" -> LiquidityAds.WillFundRates(fundingRate :: Nil, Set(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.Unknown(75), LiquidityAds.PaymentType.Unknown(211))),
)
for ((encoded, expected) <- testCases) {
val decoded = LiquidityAds.Codecs.willFundRates.decode(encoded.bits)
assert(decoded.isSuccessful)
assert(decoded.require.value == expected)
}
}
test("encode/decode all channel messages") {
val unknownTlv = GenericTlv(UInt64(5), ByteVector.fromValidHex("deadbeef"))
val msgs = List(

View file

@ -0,0 +1,65 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector64, SatoshiLong}
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.channel.{InvalidLiquidityAdsAmount, InvalidLiquidityAdsSig, MissingLiquidityAds}
import fr.acinq.eclair.{randomBytes32, randomBytes64}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.HexStringSyntax
class LiquidityAdsSpec extends AnyFunSuite {
test("validate liquidity ads funding attempt") {
val nodeKey = PrivateKey(hex"57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3")
assert(nodeKey.publicKey == PublicKey(hex"03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413"))
val fundingRate = LiquidityAds.FundingRate(100_000 sat, 1_000_000 sat, 500, 100, 10 sat, 1000 sat)
assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 500_000 sat, isChannelCreation = false).total == 5635.sat)
assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 600_000 sat, isChannelCreation = false).total == 5635.sat)
assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 600_000 sat, isChannelCreation = true).total == 6635.sat)
assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 400_000 sat, isChannelCreation = false).total == 4635.sat)
assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(10 sat)), 500_000 sat, 500_000 sat, isChannelCreation = false).total == 6260.sat)
val fundingRates = LiquidityAds.WillFundRates(fundingRate :: Nil, Set(LiquidityAds.PaymentType.FromChannelBalance))
val Some(request) = LiquidityAds.requestFunding(500_000 sat, LiquidityAds.PaymentDetails.FromChannelBalance, fundingRates)
val fundingScript = hex"00202395c9c52c02ca069f1d56a3c6124bf8b152a617328c76e6b31f83ace370c2ff"
val Right(willFund) = fundingRates.validateRequest(nodeKey, randomBytes32(), fundingScript, FeeratePerKw(1000 sat), request, isChannelCreation = true).map(_.willFund)
assert(willFund.fundingRate == fundingRate)
assert(willFund.fundingScript == fundingScript)
assert(willFund.signature == ByteVector64.fromValidHex("a53106bd20027b0215480ff0b06b2bf9324bb257c2a0e74c2604ec347493f90d3a975d56a68b21a6cc48d6763d96f70e1d630dd1720cf6b7314d4304050fe265"))
val channelId = randomBytes32()
val testCases = Seq(
(500_000 sat, Some(willFund), None),
(500_000 sat, None, Some(MissingLiquidityAds(channelId))),
(500_000 sat, Some(willFund.copy(signature = randomBytes64())), Some(InvalidLiquidityAdsSig(channelId))),
(0 sat, Some(willFund), Some(InvalidLiquidityAdsAmount(channelId, 0 sat, 500_000 sat))),
)
testCases.foreach {
case (fundingAmount, willFund_opt, failure_opt) =>
val result = request.validateRemoteFunding(nodeKey.publicKey, channelId, fundingScript, fundingAmount, FeeratePerKw(2500 sat), isChannelCreation = true, willFund_opt)
failure_opt match {
case Some(failure) => assert(result == Left(failure))
case None => assert(result.isRight)
}
}
}
}

View file

@ -63,8 +63,13 @@ trait PathFinding {
}
val nodes: Route = postRequest("nodes") { implicit t =>
formFields(nodeIdsFormParam.?) { nodeIds_opt =>
complete(eclairApi.nodes(nodeIds_opt.map(_.toSet)))
formFields(nodeIdsFormParam.?, "liquidityProvider".as[Boolean].?) { (nodeIds_opt, liquidityProviders_opt) =>
complete(eclairApi.nodes(nodeIds_opt.map(_.toSet)).map(_.filter { n =>
liquidityProviders_opt match {
case Some(true) => n.fundingRates_opt.nonEmpty
case _ => true
}
}))
}
}