From cb2183e32a7009a6c34a0a94a1521cdcd0af9f46 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 14 May 2024 11:03:36 +0200 Subject: [PATCH] Update liquidity ads to use the `payment_type` field The last commit of https://github.com/lightning/bolts/pull/1153 introduces a separate `payment_type` field, that allows extending the ways fees can be paid. --- .../eclair/wire/protocol/LiquidityAds.scala | 193 +++++++++++------- .../eclair/wire/protocol/RoutingTlv.scala | 7 +- .../wire/protocol/SetupAndControlTlv.scala | 7 +- .../eclair/wire/protocol/TlvCodecs.scala | 5 + .../protocol/LightningMessageCodecsSpec.scala | 71 +++---- 5 files changed, 162 insertions(+), 121 deletions(-) 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 45729b9a74..f0389ca0c9 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 @@ -18,14 +18,14 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream} -import fr.acinq.eclair.{MilliSatoshi, ToMilliSatoshiConversion, UInt64} +import fr.acinq.eclair.wire.protocol.TlvCodecs.{genericTlv, tlvField, tsatoshi32} +import fr.acinq.eclair.{ToMilliSatoshiConversion, UInt64} import scodec.Codec -import scodec.bits.ByteVector +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ /** @@ -39,107 +39,123 @@ import scodec.codecs._ */ object LiquidityAds { - /** - * Liquidity fees are paid using the following : - * - * - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller - * - the buyer pays [[leaseFeeProportional]] (expressed in basis points) based on the amount 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 [[fundingWeight]] vbytes of those on-chain fees - */ - case class FundingLeaseFee(fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi) { + case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) { + val total: Satoshi = miningFee + serviceFee + } + + sealed trait FundingLease extends Tlv { + def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees + } + + object FundingLease { /** - * Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding - * commitment transaction. + * Liquidity fees are paid using the following: + * + * - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller + * - the buyer pays [[leaseFeeProportional]] (expressed in basis points) based on the amount 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 [[fundingWeight]] vbytes of those on-chain fees */ - def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees = { - val onChainFees = Transactions.weight2fee(feerate, fundingWeight) - // If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity. - val proportionalFee = requestedAmount.min(contributedAmount).toMilliSatoshi * leaseFeeProportional / 10_000 - LeaseFees(onChainFees, leaseFeeBase + proportionalFee.truncateToSatoshi) + case class Basic(minAmount: Satoshi, maxAmount: Satoshi, fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi) extends FundingLease { + /** + * Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding + * commitment transaction. + */ + override def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees = { + val onChainFees = Transactions.weight2fee(feerate, fundingWeight) + // If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity. + val proportionalFee = requestedAmount.min(contributedAmount).toMilliSatoshi * leaseFeeProportional / 10_000 + LeaseFees(onChainFees, leaseFeeBase + proportionalFee.truncateToSatoshi) + } } } // @formatter:off - sealed trait FundingLease { def fundingFee: FundingLeaseFee } - case class BasicFundingLease(minAmount: Satoshi, maxAmount: Satoshi, fundingFee: FundingLeaseFee) extends FundingLease - case class DurationBasedFundingLease(leaseDuration: Int, minAmount: Satoshi, maxAmount: Satoshi, fundingFee: FundingLeaseFee, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) extends FundingLease + sealed trait FundingLeaseWitness extends Tlv + object FundingLeaseWitness { + case class Basic(fundingScript: ByteVector) extends FundingLeaseWitness + } // @formatter:on // @formatter:off - sealed trait LeaseRatesTlv extends Tlv - case class BasicFundingLeaseRates(rates: List[BasicFundingLease]) extends LeaseRatesTlv - case class DurationBasedFundingLeaseRates(rates: List[DurationBasedFundingLease]) extends LeaseRatesTlv + sealed trait PaymentType + object PaymentType { + case object FromChannelBalance extends PaymentType + case class Unknown(bitIndex: Int) extends PaymentType + // TODO: move to on-the-fly funding commit + case object FromFutureHtlc extends PaymentType + case object FromFutureHtlcWithPreimage extends PaymentType + } // @formatter:on // @formatter:off - sealed trait FundingLeaseWitness - case class BasicFundingLeaseWitness(fundingScript: ByteVector) extends FundingLeaseWitness - case class DurationBasedFundingLeaseWitness(leaseExpiry: Long, fundingScript: ByteVector, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) extends FundingLeaseWitness + sealed trait PaymentDetails extends Tlv { def paymentType: PaymentType } + object PaymentDetails { + case object FromChannelBalance extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromChannelBalance } + // TODO: move to on-the-fly funding commit + case class FromFutureHtlc(paymentHashes: List[ByteVector32]) extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromFutureHtlc } + case class FromFutureHtlcWithPreimage(preimages: List[ByteVector32]) extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromFutureHtlcWithPreimage } + } // @formatter:on - case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease) + case class WillFundRates(fundingRates: List[FundingLease], paymentTypes: Set[PaymentType]) + + case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease, paymentDetails: PaymentDetails) case class WillFund(leaseWitness: FundingLeaseWitness, signature: ByteVector64) - case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) { - val total: Satoshi = miningFee + serviceFee + def requestFunding(amount: Satoshi, paymentDetails: PaymentDetails, rates: WillFundRates): Option[RequestFunds] = { + rates.fundingRates.collectFirst { + case l: FundingLease.Basic if l.minAmount <= amount && amount <= l.maxAmount => l + } match { + case Some(l) if rates.paymentTypes.contains(paymentDetails.paymentType) => Some(RequestFunds(amount, l, paymentDetails)) + case _ => None + } } - def signLease(request: RequestFunds, nodeKey: PrivateKey, fundingScript: ByteVector, currentBlockHeight: Long): WillFund = { - val witness = request.fundingLease match { - case _: BasicFundingLease => BasicFundingLeaseWitness(fundingScript) - case l: DurationBasedFundingLease => DurationBasedFundingLeaseWitness(currentBlockHeight + l.leaseDuration, fundingScript, l.maxRelayFeeProportional, l.maxRelayFeeBase) + def validateRequest(request: RequestFunds, fundingRates: WillFundRates): Boolean = { + val paymentTypeOk = fundingRates.paymentTypes.contains(request.paymentDetails.paymentType) + val leaseOk = fundingRates.fundingRates.contains(request.fundingLease) + val amountOk = request.fundingLease match { + case lease: FundingLease.Basic => lease.minAmount <= request.requestedAmount && request.requestedAmount <= lease.maxAmount } - val toSign = witness match { - case w: BasicFundingLeaseWitness => Crypto.sha256(ByteVector("basic_funding_lease".getBytes(Charsets.US_ASCII)) ++ Codecs.basicFundingLeaseWitness.encode(w).require.bytes) - case w: DurationBasedFundingLeaseWitness => Crypto.sha256(ByteVector("duration_based_funding_lease".getBytes(Charsets.US_ASCII)) ++ Codecs.durationBasedFundingLeaseWitness.encode(w).require.bytes) + paymentTypeOk && leaseOk && amountOk + } + + def signLease(request: RequestFunds, nodeKey: PrivateKey, fundingScript: ByteVector): WillFund = { + val (tag, witness) = request.fundingLease match { + case _: FundingLease.Basic => ("basic_funding_lease", FundingLeaseWitness.Basic(fundingScript)) } + val toSign = Crypto.sha256(ByteVector(tag.getBytes(Charsets.US_ASCII)) ++ Codecs.fundingLeaseWitness.encode(witness).require.bytes) WillFund(witness, Crypto.sign(toSign, nodeKey)) } object Codecs { - private val fundingLeaseFee: Codec[FundingLeaseFee] = ( - ("fundingWeight" | uint16) :: - ("leaseFeeBasis" | uint16) :: - ("leaseFeeBase" | satoshi32) - ).as[FundingLeaseFee] - - private val basicFundingLease: Codec[BasicFundingLease] = ( + private val basicFundingLease: Codec[FundingLease.Basic] = ( ("minLeaseAmount" | satoshi32) :: ("maxLeaseAmount" | satoshi32) :: - ("leaseFee" | fundingLeaseFee) - ).as[BasicFundingLease] - - private val durationBasedFundingLease: Codec[DurationBasedFundingLease] = ( - ("leaseDuration" | uint16) :: - ("minLeaseAmount" | satoshi32) :: - ("maxLeaseAmount" | satoshi32) :: - ("leaseFee" | fundingLeaseFee) :: - ("maxChannelFeeBasis" | uint16) :: - ("maxChannelFeeBase" | millisatoshi32) - ).as[DurationBasedFundingLease] + ("fundingWeight" | uint16) :: + ("leaseFeeBasis" | uint16) :: + ("leaseFeeBase" | tsatoshi32) + ).as[FundingLease.Basic] - private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(byte) - .typecase(1, basicFundingLease) - .typecase(3, durationBasedFundingLease) + private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, basicFundingLease.complete)) - val basicFundingLeaseWitness: Codec[BasicFundingLeaseWitness] = ("fundingScript" | varsizebinarydata).as[BasicFundingLeaseWitness] + private val basicFundingLeaseWitness: Codec[FundingLeaseWitness.Basic] = ("fundingScript" | bytes).as[FundingLeaseWitness.Basic] - val durationBasedFundingLeaseWitness: Codec[DurationBasedFundingLeaseWitness] = ( - ("leaseExpiry" | uint32) :: - ("fundingScript" | varsizebinarydata) :: - ("maxChannelFeeBasis" | uint16) :: - ("maxChannelFeeBase" | millisatoshi32) - ).as[DurationBasedFundingLeaseWitness] + val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(varint) + .typecase(UInt64(0), tlvField(basicFundingLeaseWitness)) - private val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(byte) - .typecase(1, basicFundingLeaseWitness) - .typecase(3, durationBasedFundingLeaseWitness) + private val paymentDetails: Codec[PaymentDetails] = discriminated[PaymentDetails].by(varint) + .typecase(UInt64(0), tlvField(provide(PaymentDetails.FromChannelBalance))) + .typecase(UInt64(128), tlvField(("paymentHashes" | list(bytes32)).as[PaymentDetails.FromFutureHtlc])) + .typecase(UInt64(129), tlvField(("paymentPreimages" | list(bytes32)).as[PaymentDetails.FromFutureHtlcWithPreimage])) val requestFunds: Codec[RequestFunds] = ( ("requestedAmount" | satoshi) :: - ("fundingLease" | fundingLease) + ("fundingLease" | fundingLease) :: + ("paymentDetails" | paymentDetails) ).as[RequestFunds] val willFund: Codec[WillFund] = ( @@ -147,10 +163,39 @@ object LiquidityAds { ("signature" | bytes64) ).as[WillFund] - val leaseRates: Codec[TlvStream[LeaseRatesTlv]] = tlvStream(discriminated[LeaseRatesTlv].by(varint) - .typecase(UInt64(1), tlvField(list(basicFundingLease).as[BasicFundingLeaseRates])) - .typecase(UInt64(3), tlvField(list(durationBasedFundingLease).as[DurationBasedFundingLeaseRates])) + private val paymentTypes: Codec[Set[PaymentType]] = bytes.xmap( + f = { bytes => + bytes.bits.toIndexedSeq.reverse.zipWithIndex.collect { + case (true, 0) => PaymentType.FromChannelBalance + case (true, 128) => PaymentType.FromFutureHtlc + case (true, 129) => PaymentType.FromFutureHtlcWithPreimage + case (true, idx) => PaymentType.Unknown(idx) + }.toSet + }, + g = { paymentTypes => + val indexes = paymentTypes.collect { + case PaymentType.FromChannelBalance => 0 + case PaymentType.FromFutureHtlc => 128 + case PaymentType.FromFutureHtlcWithPreimage => 129 + case PaymentType.Unknown(idx) => idx + } + // When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits. + var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits + indexes.foreach { i => buf = buf.set(i) } + buf.reverse.bytes + } ) + + // We filter and ignore unknown lease types. + private val supportedFundingLeases: Codec[List[FundingLease]] = listOfN(uint16, discriminatorFallback(genericTlv, fundingLease)).xmap( + _.collect { case Right(lease) => lease }, + _.map(lease => Right(lease)), + ) + + val willFundRates: Codec[WillFundRates] = ( + ("fundingRates" | supportedFundingLeases) :: + ("paymentTypes" | variableSizeBytes(uint16, paymentTypes)) + ).as[WillFundRates] } } 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 80084ca5fb..d277989d37 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 @@ -35,13 +35,10 @@ object AnnouncementSignaturesTlv { sealed trait NodeAnnouncementTlv extends Tlv object NodeAnnouncementTlv { - case class OptionWillFund(leaseRates: TlvStream[LiquidityAds.LeaseRatesTlv]) extends NodeAnnouncementTlv { - val basicFundingRates = leaseRates.get[LiquidityAds.BasicFundingLeaseRates].map(_.rates).getOrElse(Nil) - val durationBasedFundingRates = leaseRates.get[LiquidityAds.DurationBasedFundingLeaseRates].map(_.rates).getOrElse(Nil) - } + case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends NodeAnnouncementTlv val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint) - .typecase(UInt64(1), tlvField(LiquidityAds.Codecs.leaseRates.as[OptionWillFund])) + .typecase(UInt64(1), tlvField(LiquidityAds.Codecs.willFundRates.as[OptionWillFund])) ) } 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 26d13628e1..6f01967bb3 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,10 +41,7 @@ object InitTlv { */ case class RemoteAddress(address: NodeAddress) extends InitTlv - case class OptionWillFund(leaseRates: TlvStream[LiquidityAds.LeaseRatesTlv]) extends InitTlv { - val basicFundingRates = leaseRates.get[LiquidityAds.BasicFundingLeaseRates].map(_.rates).getOrElse(Nil) - val durationBasedFundingRates = leaseRates.get[LiquidityAds.DurationBasedFundingLeaseRates].map(_.rates).getOrElse(Nil) - } + case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends InitTlv } @@ -54,7 +51,7 @@ object InitTlvCodecs { private val networks: Codec[Networks] = tlvField(list(blockHash)) private val remoteAddress: Codec[RemoteAddress] = tlvField(nodeaddress) - private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.leaseRates) + private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.willFundRates) val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint) .typecase(UInt64(1), networks) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index fda29923be..c1a13cfd5a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -98,6 +98,11 @@ object TlvCodecs { /** Truncated satoshi (0 to 8 bytes unsigned). */ val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong) + /** + * Truncated satoshi (0 to 4 bytes unsigned). + */ + val tsatoshi32: Codec[Satoshi] = tu32.xmap(l => Satoshi(l), s => s.toLong) + private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = { if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) { Attempt.Failure(Err("unknown even tlv type")) 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 2304dd7245..b68b6b818c 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 @@ -381,29 +381,21 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode liquidity ads") { - val basicRates = List( - LiquidityAds.BasicFundingLease(100_000 sat, 500_000 sat, LiquidityAds.FundingLeaseFee(550, 100, 5_000 sat)), - LiquidityAds.BasicFundingLease(500_000 sat, 5_000_000 sat, LiquidityAds.FundingLeaseFee(1100, 75, 0 sat)), - ) - val durationRates = List( - LiquidityAds.DurationBasedFundingLease(1008, 100_000 sat, 1_000_000 sat, LiquidityAds.FundingLeaseFee(600, 150, 1_000 sat), 75, 10_000 msat), - LiquidityAds.DurationBasedFundingLease(2016, 500_000 sat, 1_500_000 sat, LiquidityAds.FundingLeaseFee(1000, 250, 10_000 sat), 100, 10_000 msat), + val fundingRates = List( + LiquidityAds.FundingLease.Basic(100_000 sat, 500_000 sat, 550, 100, 5_000 sat), + LiquidityAds.FundingLease.Basic(500_000 sat, 5_000_000 sat, 1100, 75, 0 sat), ) - val liquidityAds = NodeAnnouncementTlv.OptionWillFund(TlvStream(LiquidityAds.BasicFundingLeaseRates(basicRates), LiquidityAds.DurationBasedFundingLeaseRates(durationRates))) + val liquidityAds = NodeAnnouncementTlv.OptionWillFund(LiquidityAds.WillFundRates(fundingRates, Set(LiquidityAds.PaymentType.FromChannelBalance))) val nodeKey = PrivateKey(hex"57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") val nodeAnn = Announcements.makeNodeAnnouncement(nodeKey, "LN-Liquidity", Color(42, 117, 87), Nil, Features.empty, TimestampSecond(1713171401), Some(liquidityAds)) - val nodeAnnCommonBin = hex"0101 e473dd03d777f752252af8c0829bd855cff52e56b45a92b0f26ad39bbbd714971afb3c11f708fd83fe04d39ee403fcd9765cfdb6f8b0e6dce984e50beaa15bcb 0000 661cebc9 03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413 2a7557 4c4e2d4c69717569646974790000000000000000000000000000000000000000 0000" - val basicRateBin1 = hex"000186a0 0007a120 0226 0064 00001388" - val basicRateBin2 = hex"0007a120 004c4b40 044c 004b 00000000" - // (Tag) (Length) (Rate1) (Rate2) - val basicRatesBin = hex"01" ++ hex"20" ++ basicRateBin1 ++ basicRateBin2 - val durationRateBin1 = hex"03f0 000186a0 000f4240 0258 0096 000003e8 004b 00002710" - val durationRateBin2 = hex"07e0 0007a120 0016e360 03e8 00fa 00002710 0064 00002710" - // (Tag) (Length) (Rate1) (Rate2) - val durationRatesBin = hex"03" ++ hex"30" ++ durationRateBin1 ++ durationRateBin2 - // (Tag) (Length) (Rates) - val nodeAnnTlvsBin = hex"01" ++ hex"54" ++ basicRatesBin ++ durationRatesBin + val nodeAnnCommonBin = hex"0101 4cdb99cb32b7ac3488e4c039716aa358ddad610436c23bf6b5949754cd09cc30707eccaa1dbc542d9f8fda072d5b6cb2d7b8ee8f87bfab06fdf425145bc1f563 0000 661cebc9 03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413 2a7557 4c4e2d4c69717569646974790000000000000000000000000000000000000000 0000" + val fundingRateBin1 = hex"00 0e 000186a0 0007a120 0226 0064 1388" + val fundingRateBin2 = hex"00 0c 0007a120 004c4b40 044c 004b" + // + val paymentTypesBin = hex"0001 01" + // + val nodeAnnTlvsBin = hex"01" ++ hex"23" ++ hex"0002" ++ fundingRateBin1 ++ fundingRateBin2 ++ paymentTypesBin assert(lightningMessageCodec.encode(nodeAnn).require.bytes == nodeAnnCommonBin ++ nodeAnnTlvsBin) assert(lightningMessageCodec.decode((nodeAnnCommonBin ++ nodeAnnTlvsBin).bits).require.value == nodeAnn) assert(Announcements.checkSig(nodeAnn)) @@ -416,24 +408,29 @@ class LightningMessageCodecsSpec extends AnyFunSuite { assert(lightningMessageCodec.encode(defaultAccept).require.bytes == defaultAcceptBin) val fundingScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(defaultOpen.fundingPubkey, defaultAccept.fundingPubkey))) - val currentBlockHeight = 839_322 - val requestBasic = LiquidityAds.RequestFunds(750_000 sat, basicRates.last) - val openBasic = defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestBasic))) - val openBasicBin = hex"03 19 00000000000b71b0 01 0007a120004c4b40044c004b00000000" - assert(lightningMessageCodec.encode(openBasic).require.bytes == defaultOpenBin ++ openBasicBin) - val willFundBasic = LiquidityAds.signLease(requestBasic, nodeKey, fundingScript, currentBlockHeight) - val acceptBasic = defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFundBasic))) - val acceptBasicBin = hex"03 65 01 002200202ec38203f4cf37a3b377d9a55c7ae0153c643046dbdbe2ffccfb11b74420103c 2766629334a0cd5a8835fe6fb1790fea7a85da49dab7740d72c8b591247f905a5400c576bd196e9394a92c4340179f0aaf5076b4b4953b3ca2928ded94d1dc8b" - assert(lightningMessageCodec.encode(acceptBasic).require.bytes == defaultAcceptBin ++ acceptBasicBin) - - val requestDuration = LiquidityAds.RequestFunds(750_000 sat, durationRates.head) - val openDuration = defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestDuration))) - val openDurationBin = hex"03 21 00000000000b71b0 03 03f0 000186a0 000f4240 0258 0096 000003e8 004b 00002710" - assert(lightningMessageCodec.encode(openDuration).require.bytes == defaultOpenBin ++ openDurationBin) - val willFundDuration = LiquidityAds.signLease(requestDuration, nodeKey, fundingScript, currentBlockHeight) - val acceptDuration = defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFundDuration))) - val acceptDurationBin = hex"03 6f 03 000cd28a 002200202ec38203f4cf37a3b377d9a55c7ae0153c643046dbdbe2ffccfb11b74420103c 004b 00002710 5b8e3c79db5f2144ea4d4573c033c36c601ab705afe0fab305b8b78a6551d4873ccca86d9e68ae896e722d0008c653b4c253398c266ed039c4776fd0f492d10d" - assert(lightningMessageCodec.encode(acceptDuration).require.bytes == defaultAcceptBin ++ acceptDurationBin) + val Some(request) = LiquidityAds.requestFunding(750_000 sat, LiquidityAds.PaymentDetails.FromChannelBalance, liquidityAds.rates) + assert(LiquidityAds.validateRequest(request, liquidityAds.rates)) + val open = defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(request))) + val openBin = hex"03 18 00000000000b71b0 000c0007a120004c4b40044c004b 0000" + assert(lightningMessageCodec.encode(open).require.bytes == defaultOpenBin ++ openBin) + val willFund = LiquidityAds.signLease(request, nodeKey, fundingScript) + val accept = defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))) + val acceptBin = hex"03 64 002200202ec38203f4cf37a3b377d9a55c7ae0153c643046dbdbe2ffccfb11b74420103c 2766629334a0cd5a8835fe6fb1790fea7a85da49dab7740d72c8b591247f905a5400c576bd196e9394a92c4340179f0aaf5076b4b4953b3ca2928ded94d1dc8b" + assert(lightningMessageCodec.encode(accept).require.bytes == defaultAcceptBin ++ acceptBin) + } + + test("decode unknown liquidity ads types") { + val fundingRate = LiquidityAds.FundingLease.Basic(100_000 sat, 500_000 sat, 550, 100, 5_000 sat) + val testCases = Map( + hex"0001 000e000186a00007a120022600641388 0001 01" -> LiquidityAds.WillFundRates(fundingRate :: Nil, Set(LiquidityAds.PaymentType.FromChannelBalance)), + hex"0001 000e000186a00007a120022600641388 001b 080000000000000000000000000000000008000000000000000001" -> LiquidityAds.WillFundRates(fundingRate :: Nil, Set(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.Unknown(75), LiquidityAds.PaymentType.Unknown(211))), + hex"0003 000e000186a00007a120022600641388 0104deadbeef 0204deadbeef 0000" -> LiquidityAds.WillFundRates(fundingRate :: Nil, Set.empty), + ) + for ((encoded, expected) <- testCases) { + val decoded = LiquidityAds.Codecs.willFundRates.decode(encoded.bits) + assert(decoded.isSuccessful) + assert(decoded.require.value == expected) + } } test("encode/decode all channel messages") {