diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 4eb940146e..4b1cc6955e 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -548,10 +548,33 @@ eclair { // Liquidity Ads allow remote nodes to pay us to provide them with inbound liquidity. liquidity-ads { - enabled = false // set this field to true if you want to sell your unused on-chain liquidity - fee-base-satoshis = 1000 // flat fee that we will receive every time we accept a lease request - fee-basis-points = 500 // 5% of the liquidity we will provide - max-duration-blocks = 4032 // ~1 month + // Set this field to true to activate liquidity ads and sell your available on-chain liquidity. + enabled = false + // Multiple rates can be provided, for different lease durations. + // The leased amount will be locked for that duration: the seller cannot get it back before the lease expires. + rates = [ + { + duration-blocks = 1008 // ~1 week + min-funding-amount-satoshis = 10000 // minimum funding amount we will sell + // 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 lease request + fee-basis-points = 200 // proportional fee based on the amount requested by our peer (2%) + max-channel-relay-fee-base-msat = 1000 // maximum base routing fee we will apply to that channel during the lease + max-channel-relay-fee-basis-points = 10 // maximum proportional routing fee we will apply to that channel during the lease (0.1%) + }, + { + duration-blocks = 4032 // ~1 month + min-funding-amount-satoshis = 25000 + funding-weight = 400 + fee-base-satoshis = 1000 + fee-basis-points = 500 // 5% + max-channel-relay-fee-base-msat = 5000 + max-channel-relay-fee-basis-points = 50 // 0.5% + } + ] } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a9c12fdb6e..a3c3205c82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -88,7 +88,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, onionMessageConfig: OnionMessageConfig, purgeInvoicesInterval: Option[FiniteDuration], revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config, - liquidityAdsConfig_opt: Option[LiquidityAds.Config]) { + liquidityAdsConfig_opt: Option[LiquidityAds.SellerConfig]) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -99,8 +99,6 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, val pluginOpenChannelInterceptor: Option[InterceptOpenChannelPlugin] = pluginParams.collectFirst { case p: InterceptOpenChannelPlugin => p } - val liquidityRates_opt: Option[LiquidityAds.LeaseRates] = liquidityAdsConfig_opt.map(_.leaseRates(relayParams.defaultFees(announceChannel = true))) - def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get) def currentFeerates: FeeratesPerKw = feerates.get() @@ -615,11 +613,19 @@ object NodeParams extends Logging { interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS) ), liquidityAdsConfig_opt = if (config.getBoolean("liquidity-ads.enabled")) { - Some(LiquidityAds.Config( - feeBase = Satoshi(config.getInt("liquidity-ads.fee-base-satoshis")), - feeProportional = config.getInt("liquidity-ads.fee-basis-points"), - maxLeaseDuration = config.getInt("liquidity-ads.max-duration-blocks"), - )) + Some(LiquidityAds.SellerConfig(rates = config.getConfigList("liquidity-ads.rates").asScala.map { r => + LiquidityAds.LeaseRateConfig( + rate = LiquidityAds.LeaseRate( + leaseDuration = r.getInt("duration-blocks"), + fundingWeight = r.getInt("funding-weight"), + leaseFeeProportional = r.getInt("fee-basis-points"), + leaseFeeBase = Satoshi(r.getLong("fee-base-satoshis")), + maxRelayFeeProportional = r.getInt("max-channel-relay-fee-basis-points"), + maxRelayFeeBase = MilliSatoshi(r.getLong("max-channel-relay-fee-base-msat")), + ), + minAmount = Satoshi(r.getLong("min-funding-amount-satoshis")), + ) + }.toSeq)) } else { None }, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index dbbef9613d..d510f97cf1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -78,7 +78,7 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent -case class LiquidityPurchased(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, purchase: LiquidityAds.LiquidityPurchased) extends ChannelEvent +case class LiquidityPurchased(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, isBuyer: Boolean, lease: LiquidityAds.Lease) extends ChannelEvent case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, error: ChannelError, isFatal: Boolean) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 3c9c8398ad..7ed75bc806 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -16,10 +16,10 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxId} +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, LiquidityAds, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, InteractiveTxMessage, UpdateAddHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64} import scodec.bits.ByteVector @@ -52,8 +52,10 @@ case class ChannelReserveTooHigh (override val channelId: Byte 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 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 InvalidLiquidityAdsSig (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads signature is invalid") -case class LiquidityRatesRejected (override val channelId: ByteVector32) extends ChannelException(channelId, "rejecting liquidity ads proposed rates") +case class InvalidLiquidityRates (override val channelId: ByteVector32) extends ChannelException(channelId, "rejecting liquidity ads proposed rates") +case class InvalidLiquidityAdsDuration (override val channelId: ByteVector32, leaseDuration: Int) extends ChannelException(channelId, s"rejecting liquidity ads proposed duration ($leaseDuration blocks)") 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}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9629b0fd8c..92676734c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -246,7 +246,7 @@ object Helpers { if (accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) // If we're purchasing liquidity, verify the liquidity ads: - val liquidityLease_opt = requestedFunds_opt.map(_.validateLeaseRates(remoteNodeId, accept.temporaryChannelId, accept.fundingPubkey, accept.fundingAmount, open.fundingFeerate, accept.willFund_opt) match { + val liquidityLease_opt = requestedFunds_opt.map(_.validateLease(remoteNodeId, accept.temporaryChannelId, Funding.makeFundingPubKeyScript(open.fundingPubkey, accept.fundingPubkey), accept.fundingAmount, open.fundingFeerate, accept.willFund_opt) match { case Left(t) => return Left(t) case Right(lease) => lease // we agree on liquidity rates, if any }) @@ -363,6 +363,8 @@ object Helpers { object Funding { + def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey): ByteVector = Script.write(Script.pay2wsh(Scripts.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)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 9999551c35..1159a7b317 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -950,39 +950,45 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}") val parentCommitment = d.commitments.latest.commitment val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey - val liquidityLease_opt = nodeParams.liquidityRates_opt.flatMap(_.signLease(nodeParams.privateKey, nodeParams.currentBlockHeight, localFundingPubKey, msg.feerate, msg.requestFunds_opt)) - val spliceAck = SpliceAck(d.channelId, - fundingContribution = liquidityLease_opt.map(_.lease.amount).getOrElse(0.sat), - fundingPubKey = localFundingPubKey, - pushAmount = 0.msat, - addFunding_opt = liquidityLease_opt.map(_.willFund), - 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, - liquidityPurchased_opt = liquidityLease_opt.map(l => LiquidityAds.LiquidityPurchased(isBuyer = false, l.lease)), - wallet - )) - txBuilder ! InteractiveTxBuilder.Start(self) - stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck + val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey) + LiquidityAds.offerLease_opt(nodeParams, d.channelId, fundingScript, msg.feerate, msg.requestFunds_opt) match { + case Left(t) => + log.info("rejecting splice request: {}", t.getMessage) + stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage) + case Right(liquidityLease_opt) => + val spliceAck = SpliceAck(d.channelId, + fundingContribution = liquidityLease_opt.map(_.lease.amount).getOrElse(0.sat), + fundingPubKey = localFundingPubKey, + pushAmount = 0.msat, + addFunding_opt = liquidityLease_opt.map(_.willFund), + 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, + liquidityPurchased_opt = liquidityLease_opt.map(_.lease), + 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") @@ -1010,12 +1016,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with targetFeerate = spliceInit.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs) ) - LiquidityAds.validateLeaseRates_opt(remoteNodeId, d.channelId, msg.fundingPubKey, msg.fundingContribution, spliceInit.feerate, msg.willFund_opt, cmd.requestRemoteFunding_opt) match { + val fundingScript = Funding.makeFundingPubKeyScript(spliceInit.fundingPubKey, msg.fundingPubKey) + LiquidityAds.validateLease_opt(cmd.requestRemoteFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, msg.willFund_opt) match { case Left(error) => - log.info("rejecting splice attempt: invalid lease rates") + log.info("rejecting splice attempt: {}", error.getMessage) cmd.replyTo ! RES_FAILURE(cmd, error) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, error.getMessage) - case Right(lease_opt) => + case Right(liquidityLease_opt) => val sessionId = randomBytes32() val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, @@ -1023,7 +1030,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with channelParams = d.commitments.params, purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount, - liquidityPurchased_opt = lease_opt.map(lease => LiquidityAds.LiquidityPurchased(isBuyer = true, lease)), + liquidityLease_opt, wallet )) txBuilder ! InteractiveTxBuilder.Start(self) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 568ab23296..abf71cdb8f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -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._ @@ -173,7 +174,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // and outputs. val minDepth_opt = channelParams.minDepthFundee(nodeParams.channelConf.minDepthBlocks, localAmount + remoteAmount) val upfrontShutdownScript_opt = localParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)) - val liquidityLease_opt = d.init.fundingContribution_opt.flatMap(_.signLease(nodeParams.privateKey, nodeParams.currentBlockHeight, localFundingPubkey, open.fundingFeerate, open.requestFunds_opt)) + val liquidityLease_opt = d.init.fundingContribution_opt.flatMap(_.signLease(nodeParams.privateKey, Funding.makeFundingPubKeyScript(open.fundingPubkey, localFundingPubkey), open.fundingFeerate, open.requestFunds_opt)) val tlvs: Set[AcceptDualFundedChannelTlv] = Set( upfrontShutdownScript_opt, Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)), @@ -221,7 +222,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { nodeParams, fundingParams, channelParams, purpose, localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount, - liquidityPurchased_opt = liquidityLease_opt.map(l => LiquidityAds.LiquidityPurchased(isBuyer = false, l.lease)), + liquidityPurchased_opt = liquidityLease_opt.map(_.lease), 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 @@ -285,7 +286,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { nodeParams, fundingParams, channelParams, purpose, localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount, - liquidityPurchased_opt = liquidityLease_opt.map(lease => LiquidityAds.LiquidityPurchased(isBuyer = true, lease)), + liquidityLease_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)) @@ -542,31 +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 localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, d.commitments.active.head.fundingTxIndex).publicKey - // We contribute the amount of liquidity requested by our peer, if liquidity ads is active. - val liquidityLease_opt = nodeParams.liquidityRates_opt.flatMap(_.signLease(nodeParams.privateKey, nodeParams.currentBlockHeight, localFundingPubKey, msg.feerate, msg.requestFunds_opt)) - val fundingParams = d.latestFundingTx.fundingParams.copy( - localContribution = liquidityLease_opt.map(_.lease.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, - liquidityPurchased_opt = liquidityLease_opt.map(l => LiquidityAds.LiquidityPurchased(isBuyer = false, l.lease)), - wallet)) - txBuilder ! InteractiveTxBuilder.Start(self) - val toSend = Seq( - Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding, liquidityLease_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 + val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + LiquidityAds.offerLease_opt(nodeParams, d.channelId, fundingScript, msg.feerate, msg.requestFunds_opt) match { + case Left(t) => + log.info("rejecting rbf attempt: invalid liquidity ads ({})", t.getMessage) + stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, t.getMessage) + case Right(liquidityLease_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 = liquidityLease_opt.map(_.lease.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, + liquidityPurchased_opt = liquidityLease_opt.map(_.lease), + wallet)) + txBuilder ! InteractiveTxBuilder.Start(self) + val toSend = Seq( + Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding, liquidityLease_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") @@ -592,19 +598,20 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { lockTime = cmd.lockTime, targetFeerate = cmd.targetFeerate, ) - LiquidityAds.validateLeaseRates_opt(remoteNodeId, d.channelId, fundingParams.remoteFundingPubKey, msg.fundingContribution, cmd.targetFeerate, msg.willFund_opt, cmd.requestRemoteFunding_opt) match { + val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + LiquidityAds.validateLease_opt(cmd.requestRemoteFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, cmd.targetFeerate, msg.willFund_opt) match { case Left(error) => log.info("rejecting rbf attempt: invalid lease rates") cmd.replyTo ! RES_FAILURE(cmd, error) stay() using d.copy(rbfStatus = RbfStatus.RbfAborted) sending TxAbort(d.channelId, error.getMessage) - case Right(lease_opt) => + case Right(liquidityLease_opt) => 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, - liquidityPurchased_opt = lease_opt.map(lease => LiquidityAds.LiquidityPurchased(isBuyer = true, lease)), + liquidityLease_opt, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 74a36e0e0a..59ab1f01e4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -349,7 +349,7 @@ object InteractiveTxBuilder { purpose: Purpose, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - liquidityPurchased_opt: Option[LiquidityAds.LiquidityPurchased], + liquidityPurchased_opt: Option[LiquidityAds.Lease], 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. @@ -392,7 +392,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon purpose: Purpose, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - liquidityPurchased_opt: Option[LiquidityAds.LiquidityPurchased], + liquidityPurchased_opt: Option[LiquidityAds.Lease], wallet: OnChainChannelFunder, stash: StashBuffer[InteractiveTxBuilder.Command], context: ActorContext[InteractiveTxBuilder.Command])(implicit ec: ExecutionContext) { @@ -747,8 +747,9 @@ 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 localLiquidityFee = liquidityPurchased_opt.collect { case l if l.isBuyer => l.lease.fees }.getOrElse(0 sat) - val remoteLiquidityFee = liquidityPurchased_opt.collect { case l if !l.isBuyer => l.lease.fees }.getOrElse(0 sat) + // The initiator of the interactive-tx is the liquidity buyer (if liquidity ads is used). + val localLiquidityFee = liquidityPurchased_opt.collect { case lease if fundingParams.isInitiator => lease.fees }.getOrElse(0 sat) + val remoteLiquidityFee = liquidityPurchased_opt.collect { case lease if !fundingParams.isInitiator => lease.fees }.getOrElse(0 sat) Funding.makeCommitTxs(keyManager, channelParams, fundingAmount = fundingParams.fundingAmount, toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - localLiquidityFee + remoteLiquidityFee, @@ -780,7 +781,7 @@ 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) - liquidityPurchased_opt.foreach(purchase => context.system.eventStream ! EventStream.Publish(LiquidityPurchased(replyTo.toClassic, channelParams.channelId, remoteNodeId, signedTx.txId, purchase))) + liquidityPurchased_opt.foreach(lease => context.system.eventStream ! EventStream.Publish(LiquidityPurchased(replyTo.toClassic, channelParams.channelId, remoteNodeId, signedTx.txId, isBuyer = fundingParams.isInitiator, lease))) replyTo ! Succeeded(InteractiveTxSigningSession.WaitingForSigs(fundingParams, purpose.fundingTxIndex, signedTx, Left(localCommit), remoteCommit), commitSig) Behaviors.stopped case WalletFailure(t) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index d1ba7663c1..50fc4cd635 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -282,11 +282,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(2, e.channelId.toHex) statement.setString(3, e.remoteNodeId.toHex) statement.setBoolean(4, false) // transaction isn't confirmed yet, we just published it - statement.setBoolean(5, e.purchase.isBuyer) - statement.setLong(6, e.purchase.lease.amount.toLong) - statement.setLong(7, e.purchase.lease.fees.toLong) - statement.setString(8, e.purchase.lease.sellerSig.toHex) - statement.setString(9, LiquidityAds.leaseWitnessCodec.encode(e.purchase.lease.witness).require.bytes.toHex) + statement.setBoolean(5, e.isBuyer) + statement.setLong(6, e.lease.amount.toLong) + statement.setLong(7, e.lease.fees.toLong) + statement.setString(8, e.lease.sellerSig.toHex) + statement.setString(9, LiquidityAds.LeaseWitness.codec.encode(e.lease.witness).require.bytes.toHex) statement.setTimestamp(10, Timestamp.from(Instant.now())) statement.executeUpdate() } @@ -509,7 +509,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { amount = Satoshi(rs.getLong("amount_sat")), fees = Satoshi(rs.getLong("fee_sat")), sellerSig = ByteVector64.fromValidHex(rs.getString("seller_sig")), - witness = LiquidityAds.leaseWitnessCodec.decode(rs.getByteVectorFromHex("witness").bits).require.value, + witness = LiquidityAds.LeaseWitness.codec.decode(rs.getByteVectorFromHex("witness").bits).require.value, ) ) }.toSeq diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index accfb822b2..c4a859e32c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -269,11 +269,11 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { statement.setBytes(2, e.channelId.toArray) statement.setBytes(3, e.remoteNodeId.value.toArray) statement.setBoolean(4, false) // transaction isn't confirmed yet, we just published it - statement.setBoolean(5, e.purchase.isBuyer) - statement.setLong(6, e.purchase.lease.amount.toLong) - statement.setLong(7, e.purchase.lease.fees.toLong) - statement.setBytes(8, e.purchase.lease.sellerSig.toArray) - statement.setBytes(9, LiquidityAds.leaseWitnessCodec.encode(e.purchase.lease.witness).require.bytes.toArray) + statement.setBoolean(5, e.isBuyer) + statement.setLong(6, e.lease.amount.toLong) + statement.setLong(7, e.lease.fees.toLong) + statement.setBytes(8, e.lease.sellerSig.toArray) + statement.setBytes(9, LiquidityAds.LeaseWitness.codec.encode(e.lease.witness).require.bytes.toArray) statement.setLong(10, TimestampMilli.now().toLong) statement.executeUpdate() } @@ -476,7 +476,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { amount = Satoshi(rs.getLong("amount_sat")), fees = Satoshi(rs.getLong("fee_sat")), sellerSig = ByteVector64(rs.getByteVector("seller_sig")), - witness = LiquidityAds.leaseWitnessCodec.decode(rs.getByteVector("witness").bits).require.value, + witness = LiquidityAds.LeaseWitness.codec.decode(rs.getByteVector("witness").bits).require.value, ) ) }.toSeq diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index f1d8ff7c7e..a8b3caf034 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -68,7 +68,7 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], liquidityRates_opt: Option[LiquidityAds.LeaseRates], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], liquidityRates_opt: Option[Seq[LiquidityAds.LeaseRate]], timestamp: TimestampSecond = TimestampSecond.now()): 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 { @@ -79,7 +79,7 @@ object Announcements { case address@(_: DnsHostname) => (5, address) }.sortBy(_._1).map(_._2) val tlvs: Set[NodeAnnouncementTlv] = Set( - liquidityRates_opt.map(NodeAnnouncementTlv.LiquidityAdsTlv), + liquidityRates_opt.map(r => NodeAnnouncementTlv.LiquidityAdsRates(r.toList)), ).flatten val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream(tlvs)) val sig = Crypto.sign(witness, nodeSecret) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 7b5c869f59..11a5918790 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -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(), nodeParams.liquidityRates_opt) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), nodeParams.liquidityAdsConfig_opt.map(_.rates.map(_.rate))) self ! nodeAnn log.info("initialization completed, ready to process messages") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index a8cc41dad3..d90efce4a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -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(), nodeParams.liquidityRates_opt) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), nodeParams.liquidityAdsConfig_opt.map(_.rates.map(_.rate))) handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn) } else d1 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index a7da1201b7..24ebc0adef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -65,14 +65,16 @@ object ChannelTlv { val requireConfirmedInputsCodec: Codec[RequireConfirmedInputsTlv] = tlvField(provide(RequireConfirmedInputsTlv())) /** Request inbound liquidity from our peer. */ - case class RequestFunds(amount: Satoshi, leaseExpiry: BlockHeight, leaseDuration: Int) extends OpenDualFundedChannelTlv with SpliceInitTlv with TxInitRbfTlv + case class RequestFunds(amount: Satoshi, leaseDuration: Int, leaseExpiry: BlockHeight) extends OpenDualFundedChannelTlv with SpliceInitTlv with TxInitRbfTlv - val requestFundsCodec: Codec[RequestFunds] = tlvField(satoshi :: blockHeight :: uint32.xmap(l => l.toInt, (i: Int) => i.toLong)) + val requestFundsCodec: Codec[RequestFunds] = tlvField(satoshi :: uint16 :: blockHeight) /** Liquidity rates applied to an incoming [[RequestFunds]]. */ - case class WillFund(sig: ByteVector64, leaseRates: LiquidityAds.LeaseRates) extends AcceptDualFundedChannelTlv with SpliceAckTlv with TxAckRbfTlv + case class WillFund(sig: ByteVector64, fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) extends AcceptDualFundedChannelTlv with SpliceAckTlv with TxAckRbfTlv { + def leaseRate(leaseDuration: Int): LiquidityAds.LeaseRate = LiquidityAds.LeaseRate(leaseDuration, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) + } - val willFundCodec: Codec[WillFund] = tlvField(bytes64 :: LiquidityAds.leaseRatesCodec) + val willFundCodec: Codec[WillFund] = tlvField(bytes64 :: uint16 :: uint16 :: satoshi32 :: uint16 :: millisatoshi32) case class PushAmountTlv(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 14d87cc81c..abd149ba5c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -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 liquidityRates: Seq[LiquidityAds.LeaseRate] = tlvStream.get[InitTlv.LiquidityAdsRates].map(_.leaseRates).getOrElse(Nil) } case class Warning(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[WarningTlv] = TlvStream.empty) extends SetupMessage with HasChannelId { @@ -494,20 +495,17 @@ case class NodeAnnouncement(signature: ByteVector64, alias: String, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp { - - val liquidityRates_opt: Option[LiquidityAds.LeaseRates] = tlvStream.get[NodeAnnouncementTlv.LiquidityAdsTlv].map(_.leaseRates) - 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 } + val liquidityRates: Seq[LiquidityAds.LeaseRate] = tlvStream.get[NodeAnnouncementTlv.LiquidityAdsRates].map(_.leaseRates).getOrElse(Nil) } case class ChannelUpdate(signature: ByteVector64, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala index 28d44f1aef..9daab60ba7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala @@ -17,14 +17,13 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.{ChannelException, InvalidLiquidityAdsSig, LiquidityRatesRejected, MissingLiquidityAds} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.wire.protocol.CommonCodecs.{blockHeight, millisatoshi32, publicKey, satoshi32} -import fr.acinq.eclair.wire.protocol.TlvCodecs.tmillisatoshi32 -import fr.acinq.eclair.{BlockHeight, MilliSatoshi, ToMilliSatoshiConversion} +import fr.acinq.eclair.wire.protocol.CommonCodecs.{blockHeight, millisatoshi32, satoshi32} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, NodeParams, ToMilliSatoshiConversion} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs._ @@ -42,13 +41,39 @@ import java.nio.charset.StandardCharsets */ object LiquidityAds { - val DEFAULT_LEASE_DURATION = 4032 // ~1 month + /** + * @param rate proposed lease rate. + * @param minAmount minimum funding amount for that rate: we don't want to contribute very low amounts, because that + * may lock some of our liquidity in an unconfirmed and unsafe change output. + */ + case class LeaseRateConfig(rate: LeaseRate, minAmount: Satoshi) + + case class SellerConfig(rates: Seq[LeaseRateConfig]) { + def offerLease(nodeKey: PrivateKey, channelId: ByteVector32, currentBlockHeight: BlockHeight, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, requestFunds: ChannelTlv.RequestFunds): Either[ChannelException, WillFundLease] = { + rates.find(_.rate.leaseDuration == requestFunds.leaseDuration) match { + case Some(r) => + if (currentBlockHeight + 12 < requestFunds.leaseExpiry - requestFunds.leaseDuration) { + // They're trying to cheat and pay for a smaller duration than what will actually be enforced. + Left(InvalidLiquidityAdsDuration(channelId, requestFunds.leaseDuration)) + } else if (requestFunds.amount < r.minAmount) { + Left(InvalidLiquidityAdsAmount(channelId, requestFunds.amount, r.minAmount)) + } else { + val lease = r.rate.signLease(nodeKey, fundingScript, fundingFeerate, requestFunds) + Right(lease) + } + case None => + Left(InvalidLiquidityAdsDuration(channelId, requestFunds.leaseDuration)) + } + } + } - case class Config(feeBase: Satoshi, feeProportional: Int, maxLeaseDuration: Int) { - def leaseRates(relayFees: RelayFees): LeaseRates = { - // We make the remote node pay for one p2wpkh input and one p2wpkh output. - // If we need more inputs, we will pay the fees for those additional inputs ourselves. - LeaseRates(Transactions.claimP2WPKHOutputWeight, feeProportional, (relayFees.feeProportionalMillionths / 100).toInt, feeBase, relayFees.feeBase) + def offerLease_opt(nodeParams: NodeParams, channelId: ByteVector32, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, requestFunds_opt: Option[ChannelTlv.RequestFunds]): Either[ChannelException, Option[WillFundLease]] = { + (nodeParams.liquidityAdsConfig_opt, requestFunds_opt) match { + case (Some(sellerConfig), Some(requestFunds)) => sellerConfig.offerLease(nodeParams.privateKey, channelId, nodeParams.currentBlockHeight, fundingScript, fundingFeerate, requestFunds) match { + case Left(t) => Left(t) + case Right(rates) => Right(Some(rates)) + } + case _ => Right(None) } } @@ -59,24 +84,25 @@ object LiquidityAds { /** Request inbound liquidity from a remote peer that supports liquidity ads. */ case class RequestRemoteFunding(fundingAmount: Satoshi, maxFee: Satoshi, leaseStart: BlockHeight, leaseDuration: Int) { private val leaseExpiry: BlockHeight = leaseStart + leaseDuration - val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, leaseExpiry, leaseDuration) - - def validateLeaseRates(remoteNodeId: PublicKey, - channelId: ByteVector32, - remoteFundingPubKey: PublicKey, - remoteFundingAmount: Satoshi, - fundingFeerate: FeeratePerKw, - willFund_opt: Option[ChannelTlv.WillFund]): Either[ChannelException, Lease] = { + val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, leaseDuration, leaseExpiry) + + def validateLease(remoteNodeId: PublicKey, + channelId: ByteVector32, + fundingScript: ByteVector, + remoteFundingAmount: Satoshi, + fundingFeerate: FeeratePerKw, + willFund_opt: Option[ChannelTlv.WillFund]): Either[ChannelException, Lease] = { willFund_opt match { case Some(willFund) => - val witness = LeaseWitness(remoteFundingPubKey, leaseExpiry, leaseDuration, willFund.leaseRates) - val fees = willFund.leaseRates.fees(fundingFeerate, fundingAmount, remoteFundingAmount) - if (!LeaseWitness.verify(remoteNodeId, willFund.sig, witness)) { + val leaseRate = willFund.leaseRate(leaseDuration) + val witness = LeaseWitness(fundingScript, leaseDuration, leaseExpiry, leaseRate) + val fees = leaseRate.fees(fundingFeerate, fundingAmount, remoteFundingAmount) + if (!witness.verify(remoteNodeId, willFund.sig)) { Left(InvalidLiquidityAdsSig(channelId)) - } else if (remoteFundingAmount <= 0.sat) { - Left(LiquidityRatesRejected(channelId)) + } else if (remoteFundingAmount < fundingAmount) { + Left(InvalidLiquidityAdsAmount(channelId, remoteFundingAmount, fundingAmount)) } else if (maxFee < fees) { - Left(LiquidityRatesRejected(channelId)) + Left(InvalidLiquidityRates(channelId)) } else { val leaseAmount = fundingAmount.min(remoteFundingAmount) Right(Lease(leaseAmount, fees, willFund.sig, witness)) @@ -89,15 +115,15 @@ object LiquidityAds { } } - def validateLeaseRates_opt(remoteNodeId: PublicKey, - channelId: ByteVector32, - remoteFundingPubKey: PublicKey, - remoteFundingAmount: Satoshi, - fundingFeerate: FeeratePerKw, - willFund_opt: Option[ChannelTlv.WillFund], - requestRemoteFunding_opt: Option[RequestRemoteFunding]): Either[ChannelException, Option[Lease]] = { + def validateLease_opt(requestRemoteFunding_opt: Option[RequestRemoteFunding], + remoteNodeId: PublicKey, + channelId: ByteVector32, + fundingScript: ByteVector, + remoteFundingAmount: Satoshi, + fundingFeerate: FeeratePerKw, + willFund_opt: Option[ChannelTlv.WillFund]): Either[ChannelException, Option[Lease]] = { requestRemoteFunding_opt match { - case Some(requestRemoteFunding) => requestRemoteFunding.validateLeaseRates(remoteNodeId, channelId, remoteFundingPubKey, remoteFundingAmount, fundingFeerate, willFund_opt) match { + case Some(requestRemoteFunding) => requestRemoteFunding.validateLease(remoteNodeId, channelId, fundingScript, remoteFundingAmount, fundingFeerate, willFund_opt) match { case Left(t) => Left(t) case Right(lease) => Right(Some(lease)) } @@ -106,13 +132,12 @@ object LiquidityAds { } /** We propose adding funds to a channel for an optional fee at the given rates. */ - case class AddFunding(fundingAmount: Satoshi, rates_opt: Option[LeaseRates]) { - def signLease(nodeKey: PrivateKey, - currentBlockHeight: BlockHeight, - localFundingPubKey: PublicKey, - fundingFeerate: FeeratePerKw, - requestFunds_opt: Option[ChannelTlv.RequestFunds]): Option[WillFundLease] = { - rates_opt.flatMap(_.signLease(nodeKey, currentBlockHeight, localFundingPubKey, fundingFeerate, requestFunds_opt, Some(fundingAmount))) + case class AddFunding(fundingAmount: Satoshi, leaseRate_opt: Option[LeaseRate]) { + def signLease(nodeKey: PrivateKey, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, requestFunds_opt: Option[ChannelTlv.RequestFunds]): Option[WillFundLease] = for { + rate <- leaseRate_opt + request <- requestFunds_opt + } yield { + rate.signLease(nodeKey, fundingScript, fundingFeerate, request, Some(fundingAmount)) } } @@ -121,15 +146,14 @@ object LiquidityAds { * * - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller * - the buyer pays [[leaseFeeProportional]] (expressed in basis points) of the amount contributed by the seller - * - the buyer refunds the on-chain fees for up to [[fundingWeight]] of the inputs/outputs contributed by the seller + * - the seller will have to add inputs/outputs to the transaction and pay on-chain fees for them, but the buyer + * refunds on-chain fees for [[fundingWeight]] vbytes * * The seller promises that their relay fees towards the buyer will never exceed [[maxRelayFeeBase]] and [[maxRelayFeeProportional]]. * This cannot be enforced, but if the buyer notices the seller cheating, they should blacklist them and can prove - * that they misbehaved. + * that they misbehaved using the seller's signature of the [[LeaseWitness]]. */ - case class LeaseRates(fundingWeight: Int, leaseFeeProportional: Int, maxRelayFeeProportional: Int, leaseFeeBase: Satoshi, maxRelayFeeBase: MilliSatoshi) { - val maxRelayFeeProportionalMillionths: Long = maxRelayFeeProportional.toLong * 100 - + case class LeaseRate(leaseDuration: Int, fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) { /** * Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding * commitment transaction. @@ -145,60 +169,59 @@ object LiquidityAds { * @param fundingAmountOverride_opt this field should be provided if we contribute a different amount than what was requested. */ def signLease(nodeKey: PrivateKey, - currentBlockHeight: BlockHeight, - localFundingPubKey: PublicKey, + fundingScript: ByteVector, fundingFeerate: FeeratePerKw, - requestFunds_opt: Option[ChannelTlv.RequestFunds], - fundingAmountOverride_opt: Option[Satoshi] = None): Option[WillFundLease] = { - requestFunds_opt.flatMap(requestFunds => { - if (currentBlockHeight + 12 < requestFunds.leaseExpiry - requestFunds.leaseDuration) { - // They're trying to cheat and pay for a smaller duration than what will actually be enforced. - None - } else { - val witness = LeaseWitness(localFundingPubKey, requestFunds.leaseExpiry, requestFunds.leaseDuration, this) - val sig = LeaseWitness.sign(nodeKey, witness) - val fundingAmount = fundingAmountOverride_opt.getOrElse(requestFunds.amount) - val leaseFees = fees(fundingFeerate, requestFunds.amount, fundingAmount) - val leaseAmount = fundingAmount.min(requestFunds.amount) - Some(WillFundLease(ChannelTlv.WillFund(sig, this), Lease(leaseAmount, leaseFees, sig, witness))) - } - }) + requestFunds: ChannelTlv.RequestFunds, + fundingAmountOverride_opt: Option[Satoshi] = None): WillFundLease = { + val witness = LeaseWitness(fundingScript, requestFunds.leaseDuration, requestFunds.leaseExpiry, this) + val sig = witness.sign(nodeKey) + val fundingAmount = fundingAmountOverride_opt.getOrElse(requestFunds.amount) + val leaseFees = fees(fundingFeerate, requestFunds.amount, fundingAmount) + val leaseAmount = fundingAmount.min(requestFunds.amount) + val willFund = ChannelTlv.WillFund(sig, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) + WillFundLease(willFund, Lease(leaseAmount, leaseFees, sig, witness)) } } - val leaseRatesCodec: Codec[LeaseRates] = ( - ("funding_weight" | uint16) :: - ("lease_fee_basis" | uint16) :: - ("channel_fee_basis_max" | uint16) :: - ("lease_fee_base_sat" | satoshi32) :: - ("channel_fee_max_base_msat" | tmillisatoshi32) - ).as[LeaseRates] - - /** The seller signs the lease parameters: if they cheat, the buyer can use that signature to prove they cheated. */ - case class LeaseWitness(fundingPubKey: PublicKey, leaseEnd: BlockHeight, leaseDuration: Int, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) + object LeaseRate { + val codec: Codec[LeaseRate] = ( + ("lease_duration" | uint16) :: + ("funding_weight" | uint16) :: + ("lease_fee_basis" | uint16) :: + ("lease_fee_base_sat" | satoshi32) :: + ("channel_fee_basis_max" | uint16) :: + ("channel_fee_max_base_msat" | millisatoshi32) + ).as[LeaseRate] + } - object LeaseWitness { - def apply(fundingPubKey: PublicKey, leaseEnd: BlockHeight, leaseDuration: Int, leaseRates: LeaseRates): LeaseWitness = { - LeaseWitness(fundingPubKey, leaseEnd, leaseDuration, leaseRates.maxRelayFeeProportional, leaseRates.maxRelayFeeBase) + /** + * The seller signs the lease parameters: if they raise their channel routing fees higher than what they advertised, + * the buyer can use that signature to prove that they cheated. + */ + case class LeaseWitness(fundingScript: ByteVector, leaseDuration: Int, leaseEnd: BlockHeight, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) { + def sign(nodeKey: PrivateKey): ByteVector64 = { + Crypto.sign(Crypto.sha256(LeaseWitness.codec.encode(this).require.bytes), nodeKey) } - def sign(nodeKey: PrivateKey, witness: LeaseWitness): ByteVector64 = { - Crypto.sign(Crypto.sha256(leaseWitnessCodec.encode(witness).require.bytes), nodeKey) + def verify(nodeId: PublicKey, sig: ByteVector64): Boolean = { + Crypto.verifySignature(Crypto.sha256(LeaseWitness.codec.encode(this).require.bytes), sig, nodeId) } + } - def verify(nodeId: PublicKey, sig: ByteVector64, witness: LeaseWitness): Boolean = { - Crypto.verifySignature(Crypto.sha256(leaseWitnessCodec.encode(witness).require.bytes), sig, nodeId) + object LeaseWitness { + def apply(fundingScript: ByteVector, leaseDuration: Int, leaseEnd: BlockHeight, leaseRate: LeaseRate): LeaseWitness = { + LeaseWitness(fundingScript, leaseDuration, leaseEnd, leaseRate.maxRelayFeeProportional, leaseRate.maxRelayFeeBase) } - } - val leaseWitnessCodec: Codec[LeaseWitness] = ( - ("tag" | constant(ByteVector("option_will_fund".getBytes(StandardCharsets.US_ASCII)))) :: - ("funding_pubkey" | publicKey) :: - ("lease_end" | blockHeight) :: - ("lease_duration" | uint32.xmap(l => l.toInt, (i: Int) => i.toLong)) :: - ("channel_fee_max_basis" | uint16) :: - ("channel_fee_max_base_msat" | millisatoshi32) - ).as[LeaseWitness] + val codec: Codec[LeaseWitness] = ( + ("tag" | constant(ByteVector("option_will_fund".getBytes(StandardCharsets.US_ASCII)))) :: + ("funding_script" | variableSizeBytes(uint16, bytes)) :: + ("lease_duration" | uint16) :: + ("lease_end" | blockHeight) :: + ("channel_fee_max_basis" | uint16) :: + ("channel_fee_max_base_msat" | millisatoshi32) + ).as[LeaseWitness] + } /** * Once a liquidity ads has been paid, we should keep track of the lease, and check that our peer doesn't raise their @@ -206,6 +229,7 @@ object LiquidityAds { */ case class Lease(amount: Satoshi, fees: Satoshi, sellerSig: ByteVector64, witness: LeaseWitness) { val expiry: BlockHeight = witness.leaseEnd + val maxRelayFees: RelayFees = RelayFees(witness.maxRelayFeeBase, witness.maxRelayFeeProportional.toLong * 100) } case class WillFundLease(willFund: ChannelTlv.WillFund, lease: Lease) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala index 10f0ba70df..de496779a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala @@ -36,9 +36,10 @@ sealed trait NodeAnnouncementTlv extends Tlv object NodeAnnouncementTlv { - case class LiquidityAdsTlv(leaseRates: LiquidityAds.LeaseRates) extends NodeAnnouncementTlv + /** Rates at which we sell inbound liquidity to remote peers. */ + case class LiquidityAdsRates(leaseRates: List[LiquidityAds.LeaseRate]) extends NodeAnnouncementTlv - private val liquidityAdsCodec: Codec[LiquidityAdsTlv] = tlvField(LiquidityAds.leaseRatesCodec) + private val liquidityAdsCodec: Codec[LiquidityAdsRates] = tlvField(list(LiquidityAds.LeaseRate.codec)) val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint) .typecase(UInt64(1337), liquidityAdsCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala index 6938381cb9..85dea421e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala @@ -41,6 +41,9 @@ object InitTlv { */ case class RemoteAddress(address: NodeAddress) extends InitTlv + /** Rates at which we sell inbound liquidity to remote peers. */ + case class LiquidityAdsRates(leaseRates: List[LiquidityAds.LeaseRate]) extends InitTlv + } object InitTlvCodecs { @@ -49,10 +52,12 @@ object InitTlvCodecs { private val networks: Codec[Networks] = tlvField(list(blockHash)) private val remoteAddress: Codec[RemoteAddress] = tlvField(nodeaddress) + private val liquidityAds: Codec[LiquidityAdsRates] = tlvField(list(LiquidityAds.LeaseRate.codec)) val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint) .typecase(UInt64(1), networks) .typecase(UInt64(3), remoteAddress) + .typecase(UInt64(1337), liquidityAds) ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 64d31b7a19..fd7ef4dd66 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -52,7 +52,8 @@ object TestConstants { val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat) val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat) val emptyOnionPacket: OnionRoutingPacket = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) - val defaultLiquidityRates: LiquidityAds.LeaseRates = LiquidityAds.LeaseRates(500, 100, 10, 100 sat, 200 msat) + val defaultLeaseDuration: Int = 4032 // ~1 month + val defaultLiquidityRates: LiquidityAds.LeaseRate = LiquidityAds.LeaseRate(defaultLeaseDuration, 500, 100, 100 sat, 10, 200 msat) case object TestFeature extends Feature with InitFeature with NodeFeature { val rfcName = "test_feature" @@ -229,7 +230,7 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - liquidityAdsConfig_opt = Some(LiquidityAds.Config(defaultLiquidityRates.leaseFeeBase, defaultLiquidityRates.leaseFeeProportional, LiquidityAds.DEFAULT_LEASE_DURATION)), + liquidityAdsConfig_opt = Some(LiquidityAds.SellerConfig(Seq(LiquidityAds.LeaseRateConfig(defaultLiquidityRates, minAmount = 10_000 sat)))), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( @@ -397,7 +398,7 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - liquidityAdsConfig_opt = Some(LiquidityAds.Config(defaultLiquidityRates.leaseFeeBase, defaultLiquidityRates.leaseFeeProportional, LiquidityAds.DEFAULT_LEASE_DURATION)), + liquidityAdsConfig_opt = Some(LiquidityAds.SellerConfig(Seq(LiquidityAds.LeaseRateConfig(defaultLiquidityRates, minAmount = 10_000 sat)))), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index ad72b65836..36d5ed404e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -254,7 +254,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val liquidityAds = tags.contains(ChannelStateTestsTags.LiquidityAds) val initiatorRequestRemoteFunding_opt = if (liquidityAds) { val maxFee = TestConstants.defaultLiquidityRates.fees(TestConstants.feeratePerKw, TestConstants.nonInitiatorFundingSatoshis, TestConstants.nonInitiatorFundingSatoshis) - Some(LiquidityAds.RequestRemoteFunding(TestConstants.nonInitiatorFundingSatoshis, maxFee, BlockHeight(TestConstants.defaultBlockHeight), LiquidityAds.DEFAULT_LEASE_DURATION)) + Some(LiquidityAds.RequestRemoteFunding(TestConstants.nonInitiatorFundingSatoshis, maxFee, BlockHeight(TestConstants.defaultBlockHeight), TestConstants.defaultLeaseDuration)) } else { None } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala index 33f90fb1fe..797b49cab0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala @@ -19,13 +19,14 @@ 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.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer.OpenChannelResponse +import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, LiquidityAds, OpenDualFundedChannel} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes64} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -100,7 +101,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt assert(accept.upfrontShutdownScript_opt.isEmpty) assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) assert(accept.fundingAmount == TestConstants.nonInitiatorFundingSatoshis) - assert(accept.willFund_opt.map(_.leaseRates).contains(TestConstants.defaultLiquidityRates)) + assert(accept.willFund_opt.map(_.leaseRate(TestConstants.defaultLeaseDuration)).contains(TestConstants.defaultLiquidityRates)) assert(accept.pushAmount == 0.msat) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) @@ -110,7 +111,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] - val willFundInvalidSig = ChannelTlv.WillFund(randomBytes64(), TestConstants.defaultLiquidityRates) + val willFundInvalidSig = accept.willFund_opt.get.copy(sig = randomBytes64()) val acceptInvalidSig = accept .modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.WillFund])) .modify(_.tlvStream.records).using(_ + willFundInvalidSig) @@ -123,8 +124,9 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] - val highLeaseRates = LiquidityAds.LeaseRates(500, 1000, 100, 1000 sat, 1 msat) - val Some(willFundLease) = highLeaseRates.signLease(bob.underlyingActor.nodeParams.privateKey, bob.underlyingActor.nodeParams.currentBlockHeight, accept.fundingPubkey, open.fundingFeerate, open.requestFunds_opt, None) + val highLeaseRates = LiquidityAds.LeaseRate(TestConstants.defaultLeaseDuration, 500, 1000, 1000 sat, 100, 1 msat) + val fundingScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(open.fundingPubkey, accept.fundingPubkey))) + val willFundLease = highLeaseRates.signLease(bob.underlyingActor.nodeParams.privateKey, fundingScript, open.fundingFeerate, open.requestFunds_opt.get) val acceptHighFees = accept .modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.WillFund])) .modify(_.tlvStream.records).using(_ + willFundLease.willFund) @@ -133,6 +135,15 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt 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._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala index d05e32deab..bb718c8323 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala @@ -100,21 +100,11 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] - val openWithFundsRequest = open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records + ChannelTlv.RequestFunds(50_000 sat, BlockHeight(TestConstants.defaultBlockHeight) + 2016, 2016))) + val openWithFundsRequest = open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records + ChannelTlv.RequestFunds(50_000 sat, TestConstants.defaultLeaseDuration, BlockHeight(TestConstants.defaultBlockHeight) + TestConstants.defaultLeaseDuration))) alice2bob.forward(bob, openWithFundsRequest) val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] assert(accept.willFund_opt.nonEmpty) - assert(accept.willFund_opt.map(_.leaseRates).contains(TestConstants.defaultLiquidityRates)) - } - - test("recv OpenDualFundedChannel (with invalid liquidity ads lease start)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - import f._ - - val open = alice2bob.expectMsgType[OpenDualFundedChannel] - val openWithFundsRequest = open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records + ChannelTlv.RequestFunds(50_000 sat, BlockHeight(TestConstants.defaultBlockHeight) + 4032, 2016))) - alice2bob.forward(bob, openWithFundsRequest) - val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] - assert(accept.willFund_opt.isEmpty) + assert(accept.willFund_opt.map(_.leaseRate(leaseDuration = TestConstants.defaultLeaseDuration)).contains(TestConstants.defaultLiquidityRates)) } test("recv OpenDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index ff5cfa6e7e..fc14dc058b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -73,7 +73,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture } val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - val bobLiquidityRates = bob.underlyingActor.nodeParams.liquidityRates_opt.get + val bobLiquidityRates = bob.underlyingActor.nodeParams.liquidityAdsConfig_opt.map(_.rates.head.rate).get val (requestFunding_opt, bobContribution) = if (test.tags.contains(noFundingContribution)) { (None, None) } else { @@ -387,7 +387,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val remoteFunding = TestConstants.nonInitiatorFundingSatoshis val feerate1 = TestConstants.feeratePerKw - val liquidityFee1 = bob.underlyingActor.nodeParams.liquidityRates_opt.get.fees(feerate1, remoteFunding, remoteFunding) + val liquidityFee1 = bob.underlyingActor.nodeParams.liquidityAdsConfig_opt.map(_.rates.head.rate.fees(feerate1, remoteFunding, remoteFunding)).get 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) @@ -395,16 +395,16 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture systemA.eventStream.subscribe(eventListener.ref, classOf[LiquidityPurchased]) val feerate2 = FeeratePerKw(12_500 sat) - val rbfTx = testBumpFundingFees(f, Some(feerate2), Some(LiquidityAds.RequestRemoteFunding(remoteFunding, 20_000 sat, alice.underlyingActor.nodeParams.currentBlockHeight, 2016))) - val liquidityFee2 = bob.underlyingActor.nodeParams.liquidityRates_opt.get.fees(feerate2, remoteFunding, remoteFunding) + val rbfTx = testBumpFundingFees(f, Some(feerate2), Some(LiquidityAds.RequestRemoteFunding(remoteFunding, 20_000 sat, alice.underlyingActor.nodeParams.currentBlockHeight, TestConstants.defaultLeaseDuration))) + val liquidityFee2 = bob.underlyingActor.nodeParams.liquidityAdsConfig_opt.map(_.rates.head.rate.fees(feerate2, remoteFunding, remoteFunding)).get val balanceBob2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal assert(liquidityFee1 < liquidityFee2) assert(balanceBob1 + liquidityFee2 - liquidityFee1 == balanceBob2) val event = eventListener.expectMsgType[LiquidityPurchased] assert(event.fundingTxId == rbfTx.txId) - assert(event.purchase.isBuyer) - assert(event.purchase.lease.amount == remoteFunding) - assert(event.purchase.lease.fees == liquidityFee2) + assert(event.isBuyer) + assert(event.lease.amount == remoteFunding) + assert(event.lease.fees == liquidityFee2) // The second RBF attempt removes the liquidity request. val feerate3 = FeeratePerKw(15_000 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 22f27d4026..1c9b979bfc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -313,7 +313,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik systemB.eventStream.subscribe(eventListenerB.ref, classOf[LiquidityPurchased]) val sender = TestProbe() - val fundingRequest = LiquidityAds.RequestRemoteFunding(400_000 sat, 10_000 sat, alice.nodeParams.currentBlockHeight, 2016) + val fundingRequest = LiquidityAds.RequestRemoteFunding(400_000 sat, 15_000 sat, alice.nodeParams.currentBlockHeight, TestConstants.defaultLeaseDuration) val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) alice ! cmd @@ -349,19 +349,19 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val eventA = eventListenerA.expectMsgType[LiquidityPurchased] assert(eventA.fundingTxId == spliceTx.txid) - assert(eventA.purchase.isBuyer) - assert(eventA.purchase.lease.amount == fundingRequest.fundingAmount) - assert(eventA.purchase.lease.fees > 0.sat) + assert(eventA.isBuyer) + assert(eventA.lease.amount == fundingRequest.fundingAmount) + assert(eventA.lease.fees > 0.sat) val eventB = eventListenerB.expectMsgType[LiquidityPurchased] - assert(!eventB.purchase.isBuyer) - assert(eventB.purchase.copy(isBuyer = true) == eventA.purchase) + assert(!eventB.isBuyer) + assert(eventB.lease == eventA.lease) } test("recv CMD_SPLICE (splice-in, liquidity ads, fee too high)", Tag(ChannelStateTestsTags.Quiescence)) { f => import f._ val sender = TestProbe() - val fundingRequest = LiquidityAds.RequestRemoteFunding(400_000 sat, 1_000 sat, alice.nodeParams.currentBlockHeight, 2016) + val fundingRequest = LiquidityAds.RequestRemoteFunding(400_000 sat, 1_000 sat, alice.nodeParams.currentBlockHeight, TestConstants.defaultLeaseDuration) val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) alice ! cmd @@ -380,6 +380,57 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik 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.RequestRemoteFunding(5_000 sat, 5_000 sat, alice.nodeParams.currentBlockHeight, TestConstants.defaultLeaseDuration) + 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].requestFunds_opt.nonEmpty) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("liquidity ads funding amount is too low")) + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + } + + test("recv CMD_SPLICE (splice-in, liquidity ads, invalid lease duration)", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val sender = TestProbe() + val fundingRequest = LiquidityAds.RequestRemoteFunding(100_000 sat, 20_000 sat, alice.nodeParams.currentBlockHeight, 144) + 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].requestFunds_opt.nonEmpty) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("rejecting liquidity ads proposed duration")) + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + } + + test("recv CMD_SPLICE (splice-in, liquidity ads, invalid lease start)", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val sender = TestProbe() + val fundingRequest = LiquidityAds.RequestRemoteFunding(100_000 sat, 20_000 sat, alice.nodeParams.currentBlockHeight + 144, TestConstants.defaultLeaseDuration) + 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].requestFunds_opt.nonEmpty) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAbort].toAscii.contains("rejecting liquidity ads proposed duration")) + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + } + test("recv CMD_SPLICE (splice-in, local and remote commit index mismatch)", Tag(ChannelStateTestsTags.Quiescence)) { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index d961f5ed16..08cde54597 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -138,12 +138,12 @@ class AuditDbSpec extends AnyFunSuite { val (nodeId1, nodeId2) = (randomKey().publicKey, randomKey().publicKey) val confirmedFundingTx = Transaction(2, Nil, Seq(TxOut(150_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) val unconfirmedFundingTx = Transaction(2, Nil, Seq(TxOut(100_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - val e1a = LiquidityPurchased(null, randomBytes32(), nodeId1, confirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = true, LiquidityAds.Lease(250_000 sat, 5_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(500_000), 1000, 100, 5 msat)))) - val e1b = LiquidityPurchased(null, randomBytes32(), nodeId1, confirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = false, LiquidityAds.Lease(50_000 sat, 1_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(600_000), 2000, 150, 10 msat)))) - val e1c = LiquidityPurchased(null, e1b.channelId, nodeId1, confirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = false, LiquidityAds.Lease(150_000 sat, 2_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(610_000), 1500, 100, 0 msat)))) - val e1d = LiquidityPurchased(null, randomBytes32(), nodeId1, unconfirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = true, LiquidityAds.Lease(250_000 sat, 5_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(625_000), 500, 50, 25 msat)))) - val e2a = LiquidityPurchased(null, randomBytes32(), nodeId2, confirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = false, LiquidityAds.Lease(200_000 sat, 2_500 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(500_000), 2016, 0, 1 msat)))) - val e2b = LiquidityPurchased(null, randomBytes32(), nodeId2, unconfirmedFundingTx.txid, LiquidityAds.LiquidityPurchased(isBuyer = false, LiquidityAds.Lease(200_000 sat, 2_500 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomKey().publicKey, BlockHeight(500_000), 2016, 0, 1 msat)))) + val e1a = LiquidityPurchased(null, randomBytes32(), nodeId1, confirmedFundingTx.txid, isBuyer = true, LiquidityAds.Lease(250_000 sat, 5_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(10), 1000, BlockHeight(500_000), 100, 5 msat))) + val e1b = LiquidityPurchased(null, randomBytes32(), nodeId1, confirmedFundingTx.txid, isBuyer = false, LiquidityAds.Lease(50_000 sat, 1_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(15), 2000, BlockHeight(600_000), 150, 10 msat))) + val e1c = LiquidityPurchased(null, e1b.channelId, nodeId1, confirmedFundingTx.txid, isBuyer = false, LiquidityAds.Lease(150_000 sat, 2_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(12), 1500, BlockHeight(610_000), 100, 0 msat))) + val e1d = LiquidityPurchased(null, randomBytes32(), nodeId1, unconfirmedFundingTx.txid, isBuyer = true, LiquidityAds.Lease(250_000 sat, 5_000 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(37), 500, BlockHeight(625_000), 50, 25 msat))) + val e2a = LiquidityPurchased(null, randomBytes32(), nodeId2, confirmedFundingTx.txid, isBuyer = false, LiquidityAds.Lease(200_000 sat, 2_500 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(45), 2016, BlockHeight(500_000), 0, 1 msat))) + val e2b = LiquidityPurchased(null, randomBytes32(), nodeId2, unconfirmedFundingTx.txid, isBuyer = false, LiquidityAds.Lease(200_000 sat, 2_500 sat, randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(25), 2016, BlockHeight(500_000), 0, 1 msat))) db.add(e1a) db.add(e1b) @@ -159,8 +159,8 @@ class AuditDbSpec extends AnyFunSuite { db.add(TransactionConfirmed(randomBytes32(), nodeId1, confirmedFundingTx)) db.add(TransactionConfirmed(randomBytes32(), nodeId2, confirmedFundingTx)) - assert(db.listLiquidityPurchases(nodeId1).toSet == Set(e1a, e1b, e1c).map(_.purchase)) - assert(db.listLiquidityPurchases(nodeId2) == Seq(e2a.purchase)) + assert(db.listLiquidityPurchases(nodeId1).toSet == Set(e1a, e1b, e1c).map(e => LiquidityAds.LiquidityPurchased(e.isBuyer, e.lease))) + assert(db.listLiquidityPurchases(nodeId2) == Seq(LiquidityAds.LiquidityPurchased(e2a.isBuyer, e2a.lease))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index 28652270fe..0510e7613e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -58,7 +58,7 @@ class NetworkDbSpec extends AnyFunSuite { val db = dbs.network val node_1 = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty, None) - val node_2 = Announcements.makeNodeAnnouncement(randomKey(), "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional), Some(TestConstants.defaultLiquidityRates)) + val node_2 = Announcements.makeNodeAnnouncement(randomKey(), "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional), Some(TestConstants.defaultLiquidityRates :: Nil)) val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional), None) val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-eve", Color(100.toByte, 200.toByte, 300.toByte), Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000) :: Nil, Features.empty, None) val node_5 = Announcements.makeNodeAnnouncement(randomKey(), "node-frank", Color(100.toByte, 200.toByte, 300.toByte), DnsHostname("eclair.invalid", 42000) :: Nil, Features.empty, None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 805750734a..e8ae642d8d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair.router +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong} import fr.acinq.eclair.TestConstants.Alice -import fr.acinq.eclair.{RealShortChannelId, _} 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.{LiquidityAds, NodeAddress, TlvStream} +import fr.acinq.eclair.wire.protocol.{NodeAddress, TlvStream} +import fr.acinq.eclair.{RealShortChannelId, _} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -64,7 +64,7 @@ class AnnouncementsSpec extends AnyFunSuite { Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentMetadata -> FeatureSupport.Optional, ) - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures(), Some(TestConstants.defaultLiquidityRates)) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures(), Some(TestConstants.defaultLiquidityRates :: Nil)) // Features should be filtered to only include node_announcement related features. assert(ann.features == Features( Features.DataLossProtect -> FeatureSupport.Optional, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index bee1eaf0ec..2d68c954bc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -29,8 +29,7 @@ import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.ChannelTlv._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol.LiquidityAds.LeaseRates -import fr.acinq.eclair.wire.protocol.NodeAnnouncementTlv.LiquidityAdsTlv +import fr.acinq.eclair.wire.protocol.LiquidityAds.LeaseRate import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ import fr.acinq.eclair.wire.protocol.TxRbfTlv.SharedOutputContributionTlv import org.json4s.jackson.Serialization @@ -57,28 +56,30 @@ class LightningMessageCodecsSpec extends AnyFunSuite { def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey test("encode/decode init message") { - case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[BlockHash], address: Option[IPAddress], valid: Boolean, reEncoded: Option[ByteVector] = None) + case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[BlockHash], address: Option[IPAddress], liquidityRates: Seq[LiquidityAds.LeaseRate], valid: Boolean, reEncoded: Option[ByteVector] = None) val chainHash1 = BlockHash(ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")) val chainHash2 = BlockHash(ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202")) val remoteAddress1 = IPv4(InetAddress.getByAddress(Array[Byte](140.toByte, 82.toByte, 121.toByte, 3.toByte)).asInstanceOf[Inet4Address], 9735) val remoteAddress2 = IPv6(InetAddress.getByAddress(hex"b643 8bb1 c1f9 0556 487c 0acb 2ba3 3cc2".toArray).asInstanceOf[Inet6Address], 9736) val testCases = Seq( - TestCase(hex"0000 0000", hex"", Nil, None, valid = true), // no features - TestCase(hex"0000 0002088a", hex"088a", Nil, None, valid = true), // no global features - TestCase(hex"00020200 0000", hex"0200", Nil, None, valid = true, Some(hex"0000 00020200")), // no local features - TestCase(hex"00020200 0002088a", hex"0a8a", Nil, None, valid = true, Some(hex"0000 00020a8a")), // local and global - no conflict - same size - TestCase(hex"00020200 0003020002", hex"020202", Nil, None, valid = true, Some(hex"0000 0003020202")), // local and global - no conflict - different sizes - TestCase(hex"00020a02 0002088a", hex"0a8a", Nil, None, valid = true, Some(hex"0000 00020a8a")), // local and global - conflict - same size - TestCase(hex"00022200 000302aaa2", hex"02aaa2", Nil, None, valid = true, Some(hex"0000 000302aaa2")), // local and global - conflict - different sizes - TestCase(hex"0000 0002088a 03012a05022aa2", hex"088a", Nil, None, valid = true), // unknown odd records - TestCase(hex"0000 0002088a 03012a04022aa2", hex"088a", Nil, None, valid = false), // unknown even records - TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101", hex"088a", Nil, None, valid = false), // invalid tlv stream - TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101", hex"088a", List(chainHash1), None, valid = true), // single network - TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 0307018c5279032607", hex"088a", List(chainHash1), Some(remoteAddress1), valid = true), // single network and IPv4 address - 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 0000", hex"", Nil, None, Nil, valid = true), // no features + TestCase(hex"0000 0002088a", hex"088a", Nil, None, Nil, valid = true), // no global features + TestCase(hex"00020200 0000", hex"0200", Nil, None, Nil, valid = true, Some(hex"0000 00020200")), // no local features + TestCase(hex"00020200 0002088a", hex"0a8a", Nil, None, Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - no conflict - same size + TestCase(hex"00020200 0003020002", hex"020202", Nil, None, Nil, valid = true, Some(hex"0000 0003020202")), // local and global - no conflict - different sizes + TestCase(hex"00020a02 0002088a", hex"0a8a", Nil, None, Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - conflict - same size + TestCase(hex"00022200 000302aaa2", hex"02aaa2", Nil, None, Nil, valid = true, Some(hex"0000 000302aaa2")), // local and global - conflict - different sizes + TestCase(hex"0000 0002088a 03012a05022aa2", hex"088a", Nil, None, Nil, valid = true), // unknown odd records + TestCase(hex"0000 0002088a 03012a04022aa2", hex"088a", Nil, None, Nil, valid = false), // unknown even records + TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101", hex"088a", Nil, None, Nil, valid = false), // invalid tlv stream + TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101", hex"088a", List(chainHash1), None, Nil, valid = true), // single network + TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 0307018c5279032607", hex"088a", List(chainHash1), Some(remoteAddress1), Nil, valid = true), // single network and IPv4 address + TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 031302b6438bb1c1f90556487c0acb2ba33cc22608", hex"088a", List(chainHash1), Some(remoteAddress2), Nil, valid = true), // single network and IPv6 address + TestCase(hex"0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202", hex"088a", List(chainHash1, chainHash2), None, Nil, valid = true), // multiple networks + TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 c9012a", hex"088a", List(chainHash1), None, Nil, valid = true), // network and unknown odd records + TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 02012a", hex"088a", Nil, None, Nil, valid = false), // network and unknown even records + TestCase(hex"0000 0002088a fd05391007d001f4003200000000025800000000", hex"088a", Nil, None, Seq(LiquidityAds.LeaseRate(2000, 500, 50, 0 sat, 600, 0 msat)), valid = true), // one liquidity ads + TestCase(hex"0000 0002088a fd05392003f0019000c8000061a80064000186a00fc001f401f4000027100096000249f0", hex"088a", Nil, None, Seq(LiquidityAds.LeaseRate(1008, 400, 200, 25_000 sat, 100, 100_000 msat), LiquidityAds.LeaseRate(4032, 500, 500, 10_000 sat, 150, 150_000 msat)), valid = true), // two liquidity ads ) for (testCase <- testCases) { @@ -87,6 +88,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { assert(init.features.toByteVector == testCase.rawFeatures) assert(init.networks == testCase.networks) assert(init.remoteAddress_opt == testCase.address) + assert(init.liquidityRates == testCase.liquidityRates) val encoded = initCodec.encode(init).require assert(encoded.bytes == testCase.reEncoded.getOrElse(testCase.encoded)) assert(initCodec.decode(encoded).require.value == init) @@ -200,12 +202,12 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream[TxInitRbfTlv](SharedOutputContributionTlv(1_500_000 sat), RequireConfirmedInputsTlv())) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream[TxInitRbfTlv](SharedOutputContributionTlv(0 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream[TxInitRbfTlv](SharedOutputContributionTlv(-25_000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008ffffffffffff9e58", - TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream[TxInitRbfTlv](SharedOutputContributionTlv(100_000 sat), RequestFunds(100_000 sat, BlockHeight(850_000), 4000))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 000800000000000186a0 fd05391000000000000186a0000cf85000000fa0", + TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream[TxInitRbfTlv](SharedOutputContributionTlv(100_000 sat), RequestFunds(100_000 sat, 4000, BlockHeight(850_000)))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 000800000000000186a0 fd05390e00000000000186a00fa0000cf850", TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxAckRbf(channelId2, TlvStream[TxAckRbfTlv](SharedOutputContributionTlv(450_000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008000000000006ddd0", TxAckRbf(channelId2, TlvStream[TxAckRbfTlv](SharedOutputContributionTlv(0 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 00080000000000000000", TxAckRbf(channelId2, TlvStream[TxAckRbfTlv](SharedOutputContributionTlv(-250_000 sat), RequireConfirmedInputsTlv())) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0008fffffffffffc2f70 0200", - TxAckRbf(channelId2, TlvStream[TxAckRbfTlv](SharedOutputContributionTlv(100_000 sat), WillFund(ByteVector64.Zeroes, LeaseRates(750, 150, 100, 250 sat, 0 msat)))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000800000000000186a0 fd05394a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa", + TxAckRbf(channelId2, TlvStream[TxAckRbfTlv](SharedOutputContributionTlv(100_000 sat), WillFund(ByteVector64.Zeroes, 750, 150, 250 sat, 100, 0 msat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000800000000000186a0 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000000", TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000", TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 000e 696e7465726e616c206572726f72", ) @@ -299,7 +301,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), PushAmountTlv(1105 msat))) -> (defaultEncoded ++ hex"0103401000 fe47000007020451"), defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), RequireConfirmedInputsTlv())) -> (defaultEncoded ++ hex"0103401000 0200"), - defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), RequestFunds(50_000 sat, BlockHeight(500_000), 2500))) -> (defaultEncoded ++ hex"0103401000 fd053910000000000000c3500007a120000009c4") + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), RequestFunds(50_000 sat, 2500, BlockHeight(500_000)))) -> (defaultEncoded ++ hex"0103401000 fd05390e000000000000c35009c40007a120") ) testCases.foreach { case (open, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -357,7 +359,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey()))) -> (defaultEncoded ++ hex"01021000"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), PushAmountTlv(1729 msat))) -> (defaultEncoded ++ hex"0103401000 fe470000070206c1"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey()), RequireConfirmedInputsTlv())) -> (defaultEncoded ++ hex"01021000 0200"), - defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), WillFund(ByteVector64.Zeroes, LeaseRates(750, 150, 100, 250 sat, 5 msat)))) -> (defaultEncoded ++ hex"0103401000 fd05394b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa05"), + defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), WillFund(ByteVector64.Zeroes, 750, 150, 250 sat, 100, 5 msat))) -> (defaultEncoded ++ hex"0103401000 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000005"), ) testCases.foreach { case (accept, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -391,8 +393,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val sig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val testCases = Seq( NodeAnnouncement(sig, Features.empty, 0 unixsec, publicKey(1), color, "alice", Nil) -> hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0000 00000000 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 647d4b 616c696365000000000000000000000000000000000000000000000000000000 0000", - NodeAnnouncement(sig, Features.empty, 0 unixsec, publicKey(1), color, "alice", Nil, TlvStream(LiquidityAdsTlv(LeaseRates(2000, 50, 600, 0 sat, 0 msat)))) -> hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0000 00000000 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 647d4b 616c696365000000000000000000000000000000000000000000000000000000 0000 fd05390a07d00032025800000000", - NodeAnnouncement(sig, Features.empty, 0 unixsec, publicKey(1), color, "alice", Nil, TlvStream(LiquidityAdsTlv(LeaseRates(2000, 10, 600, 25_000 sat, 100_000 msat)))) -> hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0000 00000000 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 647d4b 616c696365000000000000000000000000000000000000000000000000000000 0000 fd05390d07d0000a0258000061a80186a0", + NodeAnnouncement(sig, Features.empty, 0 unixsec, publicKey(1), color, "alice", Nil, TlvStream(NodeAnnouncementTlv.LiquidityAdsRates(LeaseRate(2000, 500, 50, 0 sat, 600, 0 msat) :: Nil))) -> hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0000 00000000 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 647d4b 616c696365000000000000000000000000000000000000000000000000000000 0000 fd05391007d001f4003200000000025800000000", + NodeAnnouncement(sig, Features.empty, 0 unixsec, publicKey(1), color, "alice", Nil, TlvStream(NodeAnnouncementTlv.LiquidityAdsRates(LeaseRate(1008, 400, 200, 25_000 sat, 100, 100_000 msat) :: LeaseRate(4032, 500, 500, 10_000 sat, 150, 150_000 msat) :: Nil))) -> hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0000 00000000 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 647d4b 616c696365000000000000000000000000000000000000000000000000000000 0000 fd05392003f0019000c8000061a80064000186a00fc001f401f4000027100096000249f0", ) testCases.foreach { case (ann, bin) => diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index f5241db3c6..a57259cc9d 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -75,7 +75,7 @@ trait ExtraDirectives extends Directives { } def withRequestedRemoteFunding: Directive1[Option[LiquidityAds.RequestRemoteFundingParams]] = formFields("requestRemoteFundingSatoshis".as[Satoshi].?, "remoteFundingMaxFeeSatoshis".as[Satoshi].?, "remoteFundingDurationBlocks".as[Int].?).tflatMap { - case (Some(requestRemoteFunding), Some(remoteFundingMaxFee), leaseDuration_opt) => provide(Some(LiquidityAds.RequestRemoteFundingParams(requestRemoteFunding, leaseDuration_opt.getOrElse(LiquidityAds.DEFAULT_LEASE_DURATION), remoteFundingMaxFee))) + case (Some(requestRemoteFunding), Some(remoteFundingMaxFee), leaseDuration_opt) => provide(Some(LiquidityAds.RequestRemoteFundingParams(requestRemoteFunding, leaseDuration_opt.getOrElse(4032 /* ~1 month */), remoteFundingMaxFee))) case (Some(_), None, _) => reject(MalformedFormFieldRejection("remoteFundingMaxFeeSatoshis", "You must specify the maximum fee you're willing to pay when requesting inbound liquidity from the remote node")) case _ => provide(None) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index e7a6491238..f262dc6701 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -66,7 +66,7 @@ trait PathFinding { 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.liquidityRates_opt.nonEmpty + case Some(true) => n.liquidityRates.nonEmpty case _ => true } }))