Skip to content

Commit

Permalink
Update liquidity ads to use the payment_type field
Browse files Browse the repository at this point in the history
The last commit of lightning/bolts#1153
introduces a separate `payment_type` field, that allows extending the
ways fees can be paid.
  • Loading branch information
t-bast committed May 14, 2024
1 parent 45c0d1a commit cb2183e
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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._

/**
Expand All @@ -39,118 +39,163 @@ 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] = (
("leaseWitness" | fundingLeaseWitness) ::
("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]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading

0 comments on commit cb2183e

Please sign in to comment.