Skip to content

Commit 45c0d1a

Browse files
committed
Add extensible liquidity ads format
Add types and codecs for the extensible liquidity ads format proposed in lightning/bolts#1153.
1 parent c493493 commit 45c0d1a

File tree

7 files changed

+249
-5
lines changed

7 files changed

+249
-5
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ object Announcements {
6868
)
6969
}
7070

71-
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = {
71+
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 = {
7272
require(alias.length <= 32)
7373
// sort addresses by ascending address descriptor type; do not reorder addresses within the same descriptor type
7474
val sortedAddresses = nodeAddresses.map {
@@ -78,7 +78,8 @@ object Announcements {
7878
case address@(_: Tor3) => (4, address)
7979
case address@(_: DnsHostname) => (5, address)
8080
}.sortBy(_._1).map(_._2)
81-
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty)
81+
val tlvs = TlvStream(Set(willFund_opt).flatten[NodeAnnouncementTlv])
82+
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, tlvs)
8283
val sig = Crypto.sign(witness, nodeSecret)
8384
NodeAnnouncement(
8485
signature = sig,
@@ -87,7 +88,8 @@ object Announcements {
8788
rgbColor = color,
8889
alias = alias,
8990
features = features.unscoped(),
90-
addresses = sortedAddresses
91+
addresses = sortedAddresses,
92+
tlvStream = tlvs
9193
)
9294
}
9395

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ object ChannelTlv {
6464

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

67+
case class RequestFundingTlv(requestFunds: LiquidityAds.RequestFunds) extends OpenDualFundedChannelTlv with TxInitRbfTlv with SpliceInitTlv
68+
69+
val requestFundingCodec: Codec[RequestFundingTlv] = tlvField(LiquidityAds.Codecs.requestFunds)
70+
71+
case class ProvideFundingTlv(willFund: LiquidityAds.WillFund) extends AcceptDualFundedChannelTlv with TxAckRbfTlv with SpliceAckTlv
72+
73+
val provideFundingCodec: Codec[ProvideFundingTlv] = tlvField(LiquidityAds.Codecs.willFund)
74+
6775
case class PushAmountTlv(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv
6876

6977
val pushAmountCodec: Codec[PushAmountTlv] = tlvField(tmillisatoshi)
@@ -99,6 +107,7 @@ object OpenDualFundedChannelTlv {
99107
.typecase(UInt64(0), upfrontShutdownScriptCodec)
100108
.typecase(UInt64(1), channelTypeCodec)
101109
.typecase(UInt64(2), requireConfirmedInputsCodec)
110+
.typecase(UInt64(3), requestFundingCodec)
102111
.typecase(UInt64(0x47000007), pushAmountCodec)
103112
)
104113
}
@@ -119,6 +128,7 @@ object TxInitRbfTlv {
119128
val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint)
120129
.typecase(UInt64(0), tlvField(satoshiSigned.as[SharedOutputContributionTlv]))
121130
.typecase(UInt64(2), requireConfirmedInputsCodec)
131+
.typecase(UInt64(3), requestFundingCodec)
122132
)
123133
}
124134

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

@@ -139,6 +150,7 @@ object SpliceInitTlv {
139150

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

150162
val spliceAckTlvCodec: Codec[TlvStream[SpliceAckTlv]] = tlvStream(discriminated[SpliceAckTlv].by(varint)
151163
.typecase(UInt64(2), requireConfirmedInputsCodec)
164+
.typecase(UInt64(3), provideFundingCodec)
152165
.typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv]))
153166
)
154167
}
@@ -165,6 +178,7 @@ object AcceptDualFundedChannelTlv {
165178
.typecase(UInt64(0), upfrontShutdownScriptCodec)
166179
.typecase(UInt64(1), channelTypeCodec)
167180
.typecase(UInt64(2), requireConfirmedInputsCodec)
181+
.typecase(UInt64(3), provideFundingCodec)
168182
.typecase(UInt64(0x47000007), pushAmountCodec)
169183
)
170184

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ object CommonCodecs {
7171
// this is needed because some millisatoshi values are encoded on 32 bits in the BOLTs
7272
// this codec will fail if the amount does not fit on 32 bits
7373
val millisatoshi32: Codec[MilliSatoshi] = uint32.xmapc(l => MilliSatoshi(l))(_.toLong)
74+
val satoshi32: Codec[Satoshi] = uint32.xmapc(l => Satoshi(l))(_.toLong)
7475

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2024 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.wire.protocol
18+
19+
import com.google.common.base.Charsets
20+
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
21+
import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, Satoshi}
22+
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
23+
import fr.acinq.eclair.transactions.Transactions
24+
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}
27+
import scodec.Codec
28+
import scodec.bits.ByteVector
29+
import scodec.codecs._
30+
31+
/**
32+
* Created by t-bast on 12/04/2024.
33+
*/
34+
35+
/**
36+
* Liquidity ads create a decentralized market for channel liquidity.
37+
* Nodes advertise fee rates for their available liquidity using the gossip protocol.
38+
* Other nodes can then pay the advertised rate to get inbound liquidity allocated towards them.
39+
*/
40+
object LiquidityAds {
41+
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) {
51+
/**
52+
* Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding
53+
* commitment transaction.
54+
*/
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)
60+
}
61+
}
62+
63+
// @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
67+
// @formatter:on
68+
69+
// @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
73+
// @formatter:on
74+
75+
// @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
79+
// @formatter:on
80+
81+
case class RequestFunds(requestedAmount: Satoshi, fundingLease: FundingLease)
82+
83+
case class WillFund(leaseWitness: FundingLeaseWitness, signature: ByteVector64)
84+
85+
case class LeaseFees(miningFee: Satoshi, serviceFee: Satoshi) {
86+
val total: Satoshi = miningFee + serviceFee
87+
}
88+
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)
93+
}
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)
97+
}
98+
WillFund(witness, Crypto.sign(toSign, nodeKey))
99+
}
100+
101+
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] = (
109+
("minLeaseAmount" | satoshi32) ::
110+
("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]
122+
123+
private val fundingLease: Codec[FundingLease] = discriminated[FundingLease].by(byte)
124+
.typecase(1, basicFundingLease)
125+
.typecase(3, durationBasedFundingLease)
126+
127+
val basicFundingLeaseWitness: Codec[BasicFundingLeaseWitness] = ("fundingScript" | varsizebinarydata).as[BasicFundingLeaseWitness]
128+
129+
val durationBasedFundingLeaseWitness: Codec[DurationBasedFundingLeaseWitness] = (
130+
("leaseExpiry" | uint32) ::
131+
("fundingScript" | varsizebinarydata) ::
132+
("maxChannelFeeBasis" | uint16) ::
133+
("maxChannelFeeBase" | millisatoshi32)
134+
).as[DurationBasedFundingLeaseWitness]
135+
136+
private val fundingLeaseWitness: Codec[FundingLeaseWitness] = discriminated[FundingLeaseWitness].by(byte)
137+
.typecase(1, basicFundingLeaseWitness)
138+
.typecase(3, durationBasedFundingLeaseWitness)
139+
140+
val requestFunds: Codec[RequestFunds] = (
141+
("requestedAmount" | satoshi) ::
142+
("fundingLease" | fundingLease)
143+
).as[RequestFunds]
144+
145+
val willFund: Codec[WillFund] = (
146+
("leaseWitness" | fundingLeaseWitness) ::
147+
("signature" | bytes64)
148+
).as[WillFund]
149+
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]))
153+
)
154+
}
155+
156+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ object AnnouncementSignaturesTlv {
3535
sealed trait NodeAnnouncementTlv extends Tlv
3636

3737
object NodeAnnouncementTlv {
38-
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint))
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+
}
42+
43+
val nodeAnnouncementTlvCodec: Codec[TlvStream[NodeAnnouncementTlv]] = tlvStream(discriminated[NodeAnnouncementTlv].by(varint)
44+
.typecase(UInt64(1), tlvField(LiquidityAds.Codecs.leaseRates.as[OptionWillFund]))
45+
)
3946
}
4047

4148
sealed trait ChannelAnnouncementTlv extends Tlv

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ 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+
}
48+
4449
}
4550

4651
object InitTlvCodecs {
@@ -49,10 +54,12 @@ object InitTlvCodecs {
4954

5055
private val networks: Codec[Networks] = tlvField(list(blockHash))
5156
private val remoteAddress: Codec[RemoteAddress] = tlvField(nodeaddress)
57+
private val willFund: Codec[OptionWillFund] = tlvField(LiquidityAds.Codecs.leaseRates)
5258

5359
val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint)
5460
.typecase(UInt64(1), networks)
5561
.typecase(UInt64(3), remoteAddress)
62+
.typecase(UInt64(5), willFund)
5663
)
5764

5865
}

0 commit comments

Comments
 (0)