Skip to content

Commit

Permalink
Add extensible liquidity ads format
Browse files Browse the repository at this point in the history
Add types and codecs for the extensible liquidity ads format proposed
in lightning/bolts#1153.
  • Loading branch information
t-bast committed May 13, 2024
1 parent c493493 commit 45c0d1a
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ object Announcements {
)
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ object ChannelTlv {

val requireConfirmedInputsCodec: Codec[RequireConfirmedInputsTlv] = tlvField(provide(RequireConfirmedInputsTlv()))

case class RequestFundingTlv(requestFunds: LiquidityAds.RequestFunds) extends OpenDualFundedChannelTlv with TxInitRbfTlv with SpliceInitTlv

val requestFundingCodec: Codec[RequestFundingTlv] = tlvField(LiquidityAds.Codecs.requestFunds)

case class ProvideFundingTlv(willFund: LiquidityAds.WillFund) extends AcceptDualFundedChannelTlv with TxAckRbfTlv with SpliceAckTlv

val provideFundingCodec: Codec[ProvideFundingTlv] = tlvField(LiquidityAds.Codecs.willFund)

case class PushAmountTlv(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv

val pushAmountCodec: Codec[PushAmountTlv] = tlvField(tmillisatoshi)
Expand Down Expand Up @@ -99,6 +107,7 @@ object OpenDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), requestFundingCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)
}
Expand All @@ -119,6 +128,7 @@ object TxInitRbfTlv {
val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint)
.typecase(UInt64(0), tlvField(satoshiSigned.as[SharedOutputContributionTlv]))
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), requestFundingCodec)
)
}

Expand All @@ -130,6 +140,7 @@ object TxAckRbfTlv {
val txAckRbfTlvCodec: Codec[TlvStream[TxAckRbfTlv]] = tlvStream(discriminated[TxAckRbfTlv].by(varint)
.typecase(UInt64(0), tlvField(satoshiSigned.as[SharedOutputContributionTlv]))
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), provideFundingCodec)
)
}

Expand All @@ -139,6 +150,7 @@ object SpliceInitTlv {

val spliceInitTlvCodec: Codec[TlvStream[SpliceInitTlv]] = tlvStream(discriminated[SpliceInitTlv].by(varint)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), requestFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
)
}
Expand All @@ -149,6 +161,7 @@ object SpliceAckTlv {

val spliceAckTlvCodec: Codec[TlvStream[SpliceAckTlv]] = tlvStream(discriminated[SpliceAckTlv].by(varint)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), provideFundingCodec)
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
)
}
Expand All @@ -165,6 +178,7 @@ object AcceptDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(3), provideFundingCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)

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

val timestampSecond: Codec[TimestampSecond] = uint32.xmapc(TimestampSecond(_))(_.toLong)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2024 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.wire.protocol

import com.google.common.base.Charsets
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{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 scodec.Codec
import scodec.bits.ByteVector
import scodec.codecs._

/**
* Created by t-bast on 12/04/2024.
*/

/**
* Liquidity ads create a decentralized market for channel liquidity.
* Nodes advertise fee rates for their available liquidity using the gossip protocol.
* Other nodes can then pay the advertised rate to get inbound liquidity allocated towards them.
*/
object LiquidityAds {

/**
* 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) {
/**
* Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding
* commitment transaction.
*/
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
// @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
// @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
// @formatter:on

case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease)

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

case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) {
val total: Satoshi = miningFee + serviceFee
}

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)
}
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)
}
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] = (
("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]

private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(byte)
.typecase(1, basicFundingLease)
.typecase(3, durationBasedFundingLease)

val basicFundingLeaseWitness: Codec[BasicFundingLeaseWitness] = ("fundingScript" | varsizebinarydata).as[BasicFundingLeaseWitness]

val durationBasedFundingLeaseWitness: Codec[DurationBasedFundingLeaseWitness] = (
("leaseExpiry" | uint32) ::
("fundingScript" | varsizebinarydata) ::
("maxChannelFeeBasis" | uint16) ::
("maxChannelFeeBase" | millisatoshi32)
).as[DurationBasedFundingLeaseWitness]

private val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(byte)
.typecase(1, basicFundingLeaseWitness)
.typecase(3, durationBasedFundingLeaseWitness)

val requestFunds: Codec[RequestFunds] = (
("requestedAmount" | satoshi) ::
("fundingLease" | fundingLease)
).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]))
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ object AnnouncementSignaturesTlv {
sealed trait NodeAnnouncementTlv extends Tlv

object NodeAnnouncementTlv {
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint))
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)
}

val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint)
.typecase(UInt64(1), tlvField(LiquidityAds.Codecs.leaseRates.as[OptionWillFund]))
)
}

sealed trait ChannelAnnouncementTlv extends Tlv
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ 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)
}

}

object InitTlvCodecs {
Expand All @@ -49,10 +54,12 @@ 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)

val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint)
.typecase(UInt64(1), networks)
.typecase(UInt64(3), remoteAddress)
.typecase(UInt64(5), willFund)
)

}
Expand Down
Loading

0 comments on commit 45c0d1a

Please sign in to comment.