Skip to content

Commit cb2183e

Browse files
committed
Update liquidity ads to use the payment_type field
The last commit of lightning/bolts#1153 introduces a separate `payment_type` field, that allows extending the ways fees can be paid.
1 parent 45c0d1a commit cb2183e

File tree

5 files changed

+162
-121
lines changed

5 files changed

+162
-121
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala

Lines changed: 119 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ package fr.acinq.eclair.wire.protocol
1818

1919
import com.google.common.base.Charsets
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
21-
import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, Satoshi}
21+
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi}
2222
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2323
import fr.acinq.eclair.transactions.Transactions
2424
import fr.acinq.eclair.wire.protocol.CommonCodecs._
25-
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream}
26-
import fr.acinq.eclair.{MilliSatoshi, ToMilliSatoshiConversion, UInt64}
25+
import fr.acinq.eclair.wire.protocol.TlvCodecs.{genericTlv, tlvField, tsatoshi32}
26+
import fr.acinq.eclair.{ToMilliSatoshiConversion, UInt64}
2727
import scodec.Codec
28-
import scodec.bits.ByteVector
28+
import scodec.bits.{BitVector, ByteVector}
2929
import scodec.codecs._
3030

3131
/**
@@ -39,118 +39,163 @@ import scodec.codecs._
3939
*/
4040
object LiquidityAds {
4141

42-
/**
43-
* Liquidity fees are paid using the following :
44-
*
45-
* - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller
46-
* - the buyer pays [[leaseFeeProportional]] (expressed in basis points) based on the amount contributed by the seller
47-
* - the seller will have to add inputs/outputs to the transaction and pay on-chain fees for them, but the buyer
48-
* refunds [[fundingWeight]] vbytes of those on-chain fees
49-
*/
50-
case class FundingLeaseFee(fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi) {
42+
case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) {
43+
val total: Satoshi = miningFee + serviceFee
44+
}
45+
46+
sealed trait FundingLease extends Tlv {
47+
def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees
48+
}
49+
50+
object FundingLease {
5151
/**
52-
* Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding
53-
* commitment transaction.
52+
* Liquidity fees are paid using the following:
53+
*
54+
* - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller
55+
* - the buyer pays [[leaseFeeProportional]] (expressed in basis points) based on the amount contributed by the seller
56+
* - the seller will have to add inputs/outputs to the transaction and pay on-chain fees for them, but the buyer
57+
* refunds [[fundingWeight]] vbytes of those on-chain fees
5458
*/
55-
def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees = {
56-
val onChainFees = Transactions.weight2fee(feerate, fundingWeight)
57-
// If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity.
58-
val proportionalFee = requestedAmount.min(contributedAmount).toMilliSatoshi * leaseFeeProportional / 10_000
59-
LeaseFees(onChainFees, leaseFeeBase + proportionalFee.truncateToSatoshi)
59+
case class Basic(minAmount: Satoshi, maxAmount: Satoshi, fundingWeight: Int, leaseFeeProportional: Int, leaseFeeBase: Satoshi) extends FundingLease {
60+
/**
61+
* Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding
62+
* commitment transaction.
63+
*/
64+
override def fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees = {
65+
val onChainFees = Transactions.weight2fee(feerate, fundingWeight)
66+
// If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity.
67+
val proportionalFee = requestedAmount.min(contributedAmount).toMilliSatoshi * leaseFeeProportional / 10_000
68+
LeaseFees(onChainFees, leaseFeeBase + proportionalFee.truncateToSatoshi)
69+
}
6070
}
6171
}
6272

6373
// @formatter:off
64-
sealed trait FundingLease { def fundingFee: FundingLeaseFee }
65-
case class BasicFundingLease(minAmount: Satoshi, maxAmount: Satoshi, fundingFee: FundingLeaseFee) extends FundingLease
66-
case class DurationBasedFundingLease(leaseDuration: Int, minAmount: Satoshi, maxAmount: Satoshi, fundingFee: FundingLeaseFee, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) extends FundingLease
74+
sealed trait FundingLeaseWitness extends Tlv
75+
object FundingLeaseWitness {
76+
case class Basic(fundingScript: ByteVector) extends FundingLeaseWitness
77+
}
6778
// @formatter:on
6879

6980
// @formatter:off
70-
sealed trait LeaseRatesTlv extends Tlv
71-
case class BasicFundingLeaseRates(rates: List[BasicFundingLease]) extends LeaseRatesTlv
72-
case class DurationBasedFundingLeaseRates(rates: List[DurationBasedFundingLease]) extends LeaseRatesTlv
81+
sealed trait PaymentType
82+
object PaymentType {
83+
case object FromChannelBalance extends PaymentType
84+
case class Unknown(bitIndex: Int) extends PaymentType
85+
// TODO: move to on-the-fly funding commit
86+
case object FromFutureHtlc extends PaymentType
87+
case object FromFutureHtlcWithPreimage extends PaymentType
88+
}
7389
// @formatter:on
7490

7591
// @formatter:off
76-
sealed trait FundingLeaseWitness
77-
case class BasicFundingLeaseWitness(fundingScript: ByteVector) extends FundingLeaseWitness
78-
case class DurationBasedFundingLeaseWitness(leaseExpiry: Long, fundingScript: ByteVector, maxRelayFeeProportional: Int, maxRelayFeeBase: MilliSatoshi) extends FundingLeaseWitness
92+
sealed trait PaymentDetails extends Tlv { def paymentType: PaymentType }
93+
object PaymentDetails {
94+
case object FromChannelBalance extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromChannelBalance }
95+
// TODO: move to on-the-fly funding commit
96+
case class FromFutureHtlc(paymentHashes: List[ByteVector32]) extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromFutureHtlc }
97+
case class FromFutureHtlcWithPreimage(preimages: List[ByteVector32]) extends PaymentDetails { override val paymentType: PaymentType = PaymentType.FromFutureHtlcWithPreimage }
98+
}
7999
// @formatter:on
80100

81-
case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease)
101+
case class WillFundRates(fundingRates: List[FundingLease], paymentTypes: Set[PaymentType])
102+
103+
case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease, paymentDetails: PaymentDetails)
82104

83105
case class WillFund(leaseWitness: FundingLeaseWitness, signature: ByteVector64)
84106

85-
case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) {
86-
val total: Satoshi = miningFee + serviceFee
107+
def requestFunding(amount: Satoshi, paymentDetails: PaymentDetails, rates: WillFundRates): Option[RequestFunds] = {
108+
rates.fundingRates.collectFirst {
109+
case l: FundingLease.Basic if l.minAmount <= amount && amount <= l.maxAmount => l
110+
} match {
111+
case Some(l) if rates.paymentTypes.contains(paymentDetails.paymentType) => Some(RequestFunds(amount, l, paymentDetails))
112+
case _ => None
113+
}
87114
}
88115

89-
def signLease(request: RequestFunds, nodeKey: PrivateKey, fundingScript: ByteVector, currentBlockHeight: Long): WillFund = {
90-
val witness = request.fundingLease match {
91-
case _: BasicFundingLease => BasicFundingLeaseWitness(fundingScript)
92-
case l: DurationBasedFundingLease => DurationBasedFundingLeaseWitness(currentBlockHeight + l.leaseDuration, fundingScript, l.maxRelayFeeProportional, l.maxRelayFeeBase)
116+
def validateRequest(request: RequestFunds, fundingRates: WillFundRates): Boolean = {
117+
val paymentTypeOk = fundingRates.paymentTypes.contains(request.paymentDetails.paymentType)
118+
val leaseOk = fundingRates.fundingRates.contains(request.fundingLease)
119+
val amountOk = request.fundingLease match {
120+
case lease: FundingLease.Basic => lease.minAmount <= request.requestedAmount && request.requestedAmount <= lease.maxAmount
93121
}
94-
val toSign = witness match {
95-
case w: BasicFundingLeaseWitness => Crypto.sha256(ByteVector("basic_funding_lease".getBytes(Charsets.US_ASCII)) ++ Codecs.basicFundingLeaseWitness.encode(w).require.bytes)
96-
case w: DurationBasedFundingLeaseWitness => Crypto.sha256(ByteVector("duration_based_funding_lease".getBytes(Charsets.US_ASCII)) ++ Codecs.durationBasedFundingLeaseWitness.encode(w).require.bytes)
122+
paymentTypeOk && leaseOk && amountOk
123+
}
124+
125+
def signLease(request: RequestFunds, nodeKey: PrivateKey, fundingScript: ByteVector): WillFund = {
126+
val (tag, witness) = request.fundingLease match {
127+
case _: FundingLease.Basic => ("basic_funding_lease", FundingLeaseWitness.Basic(fundingScript))
97128
}
129+
val toSign = Crypto.sha256(ByteVector(tag.getBytes(Charsets.US_ASCII)) ++ Codecs.fundingLeaseWitness.encode(witness).require.bytes)
98130
WillFund(witness, Crypto.sign(toSign, nodeKey))
99131
}
100132

101133
object Codecs {
102-
private val fundingLeaseFee: Codec[FundingLeaseFee] = (
103-
("fundingWeight" | uint16) ::
104-
("leaseFeeBasis" | uint16) ::
105-
("leaseFeeBase" | satoshi32)
106-
).as[FundingLeaseFee]
107-
108-
private val basicFundingLease: Codec[BasicFundingLease] = (
134+
private val basicFundingLease: Codec[FundingLease.Basic] = (
109135
("minLeaseAmount" | satoshi32) ::
110136
("maxLeaseAmount" | satoshi32) ::
111-
("leaseFee" | fundingLeaseFee)
112-
).as[BasicFundingLease]
113-
114-
private val durationBasedFundingLease: Codec[DurationBasedFundingLease] = (
115-
("leaseDuration" | uint16) ::
116-
("minLeaseAmount" | satoshi32) ::
117-
("maxLeaseAmount" | satoshi32) ::
118-
("leaseFee" | fundingLeaseFee) ::
119-
("maxChannelFeeBasis" | uint16) ::
120-
("maxChannelFeeBase" | millisatoshi32)
121-
).as[DurationBasedFundingLease]
137+
("fundingWeight" | uint16) ::
138+
("leaseFeeBasis" | uint16) ::
139+
("leaseFeeBase" | tsatoshi32)
140+
).as[FundingLease.Basic]
122141

123-
private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(byte)
124-
.typecase(1, basicFundingLease)
125-
.typecase(3, durationBasedFundingLease)
142+
private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(varint)
143+
.typecase(UInt64(0), variableSizeBytesLong(varintoverflow, basicFundingLease.complete))
126144

127-
val basicFundingLeaseWitness: Codec[BasicFundingLeaseWitness] = ("fundingScript" | varsizebinarydata).as[BasicFundingLeaseWitness]
145+
private val basicFundingLeaseWitness: Codec[FundingLeaseWitness.Basic] = ("fundingScript" | bytes).as[FundingLeaseWitness.Basic]
128146

129-
val durationBasedFundingLeaseWitness: Codec[DurationBasedFundingLeaseWitness] = (
130-
("leaseExpiry" | uint32) ::
131-
("fundingScript" | varsizebinarydata) ::
132-
("maxChannelFeeBasis" | uint16) ::
133-
("maxChannelFeeBase" | millisatoshi32)
134-
).as[DurationBasedFundingLeaseWitness]
147+
val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(varint)
148+
.typecase(UInt64(0), tlvField(basicFundingLeaseWitness))
135149

136-
private val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(byte)
137-
.typecase(1, basicFundingLeaseWitness)
138-
.typecase(3, durationBasedFundingLeaseWitness)
150+
private val paymentDetails: Codec[PaymentDetails] = discriminated[PaymentDetails].by(varint)
151+
.typecase(UInt64(0), tlvField(provide(PaymentDetails.FromChannelBalance)))
152+
.typecase(UInt64(128), tlvField(("paymentHashes" | list(bytes32)).as[PaymentDetails.FromFutureHtlc]))
153+
.typecase(UInt64(129), tlvField(("paymentPreimages" | list(bytes32)).as[PaymentDetails.FromFutureHtlcWithPreimage]))
139154

140155
val requestFunds: Codec[RequestFunds] = (
141156
("requestedAmount" | satoshi) ::
142-
("fundingLease" | fundingLease)
157+
("fundingLease" | fundingLease) ::
158+
("paymentDetails" | paymentDetails)
143159
).as[RequestFunds]
144160

145161
val willFund: Codec[WillFund] = (
146162
("leaseWitness" | fundingLeaseWitness) ::
147163
("signature" | bytes64)
148164
).as[WillFund]
149165

150-
val leaseRates: Codec[TlvStream[LeaseRatesTlv]] = tlvStream(discriminated[LeaseRatesTlv].by(varint)
151-
.typecase(UInt64(1), tlvField(list(basicFundingLease).as[BasicFundingLeaseRates]))
152-
.typecase(UInt64(3), tlvField(list(durationBasedFundingLease).as[DurationBasedFundingLeaseRates]))
166+
private val paymentTypes: Codec[Set[PaymentType]] = bytes.xmap(
167+
f = { bytes =>
168+
bytes.bits.toIndexedSeq.reverse.zipWithIndex.collect {
169+
case (true, 0) => PaymentType.FromChannelBalance
170+
case (true, 128) => PaymentType.FromFutureHtlc
171+
case (true, 129) => PaymentType.FromFutureHtlcWithPreimage
172+
case (true, idx) => PaymentType.Unknown(idx)
173+
}.toSet
174+
},
175+
g = { paymentTypes =>
176+
val indexes = paymentTypes.collect {
177+
case PaymentType.FromChannelBalance => 0
178+
case PaymentType.FromFutureHtlc => 128
179+
case PaymentType.FromFutureHtlcWithPreimage => 129
180+
case PaymentType.Unknown(idx) => idx
181+
}
182+
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits.
183+
var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits
184+
indexes.foreach { i => buf = buf.set(i) }
185+
buf.reverse.bytes
186+
}
153187
)
188+
189+
// We filter and ignore unknown lease types.
190+
private val supportedFundingLeases: Codec[List[FundingLease]] = listOfN(uint16, discriminatorFallback(genericTlv, fundingLease)).xmap(
191+
_.collect { case Right(lease) => lease },
192+
_.map(lease => Right(lease)),
193+
)
194+
195+
val willFundRates: Codec[WillFundRates] = (
196+
("fundingRates" | supportedFundingLeases) ::
197+
("paymentTypes" | variableSizeBytes(uint16, paymentTypes))
198+
).as[WillFundRates]
154199
}
155200

156201
}

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,10 @@ object AnnouncementSignaturesTlv {
3535
sealed trait NodeAnnouncementTlv extends Tlv
3636

3737
object NodeAnnouncementTlv {
38-
case class OptionWillFund(leaseRates: TlvStream[LiquidityAds.LeaseRatesTlv]) extends NodeAnnouncementTlv {
39-
val basicFundingRates = leaseRates.get[LiquidityAds.BasicFundingLeaseRates].map(_.rates).getOrElse(Nil)
40-
val durationBasedFundingRates = leaseRates.get[LiquidityAds.DurationBasedFundingLeaseRates].map(_.rates).getOrElse(Nil)
41-
}
38+
case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends NodeAnnouncementTlv
4239

4340
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint)
44-
.typecase(UInt64(1), tlvField(LiquidityAds.Codecs.leaseRates.as[OptionWillFund]))
41+
.typecase(UInt64(1), tlvField(LiquidityAds.Codecs.willFundRates.as[OptionWillFund]))
4542
)
4643
}
4744

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ object InitTlv {
4141
*/
4242
case class RemoteAddress(address: NodeAddress) extends InitTlv
4343

44-
case class OptionWillFund(leaseRates: TlvStream[LiquidityAds.LeaseRatesTlv]) extends InitTlv {
45-
val basicFundingRates = leaseRates.get[LiquidityAds.BasicFundingLeaseRates].map(_.rates).getOrElse(Nil)
46-
val durationBasedFundingRates = leaseRates.get[LiquidityAds.DurationBasedFundingLeaseRates].map(_.rates).getOrElse(Nil)
47-
}
44+
case class OptionWillFund(rates: LiquidityAds.WillFundRates) extends InitTlv
4845

4946
}
5047

@@ -54,7 +51,7 @@ object InitTlvCodecs {
5451

5552
private val networks: Codec[Networks] = tlvField(list(blockHash))
5653
private val remoteAddress: Codec[RemoteAddress] = tlvField(nodeaddress)
57-
private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.leaseRates)
54+
private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.willFundRates)
5855

5956
val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint)
6057
.typecase(UInt64(1), networks)

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ object TlvCodecs {
9898
/** Truncated satoshi (0 to 8 bytes unsigned). */
9999
val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong)
100100

101+
/**
102+
* Truncated satoshi (0 to 4 bytes unsigned).
103+
*/
104+
val tsatoshi32: Codec[Satoshi] = tu32.xmap(l => Satoshi(l), s => s.toLong)
105+
101106
private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = {
102107
if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) {
103108
Attempt.Failure(Err("unknown even tlv type"))

0 commit comments

Comments
 (0)