Skip to content

Commit f8c6095

Browse files
committed
Announce liquidity ads
Configure liquidity ads and include it in our `node_announcement`.
1 parent dc5ffe4 commit f8c6095

File tree

19 files changed

+140
-112
lines changed

19 files changed

+140
-112
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ This feature leaks a bit of information about the balance when the channel is al
5555
### API changes
5656

5757
- `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743)
58+
- `nodes` allows filtering nodes that offer liquidity ads (#2550)
5859

5960
### Miscellaneous improvements and bug fixes
6061

eclair-core/src/main/resources/reference.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ eclair {
533533
enabled = true // enable automatic purges of expired invoices from the database
534534
interval = 24 hours // interval between expired invoice purges
535535
}
536+
537+
// Liquidity Ads allow remote nodes to pay us to provide them with inbound liquidity.
538+
liquidity-ads {
539+
enabled = false // set this field to true if you want to sell your unused on-chain liquidity
540+
fee-base-satoshis = 1000 // flat fee that we will receive every time we accept a lease request
541+
fee-basis-points = 500 // 5% of the liquidity we will provide
542+
max-duration-blocks = 4032 // ~1 month
543+
}
536544
}
537545

538546
akka {

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
8686
blockchainWatchdogThreshold: Int,
8787
blockchainWatchdogSources: Seq[String],
8888
onionMessageConfig: OnionMessageConfig,
89-
purgeInvoicesInterval: Option[FiniteDuration]) {
89+
purgeInvoicesInterval: Option[FiniteDuration],
90+
liquidityAdsConfig_opt: Option[LiquidityAds.Config]) {
9091
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey
9192

9293
val nodeId: PublicKey = nodeKeyManager.nodeId
@@ -97,6 +98,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
9798

9899
val pluginOpenChannelInterceptor: Option[InterceptOpenChannelPlugin] = pluginParams.collectFirst { case p: InterceptOpenChannelPlugin => p }
99100

101+
val liquidityRates_opt: Option[LiquidityAds.LeaseRates] = liquidityAdsConfig_opt.map(_.leaseRates(relayParams.defaultFees(announceChannel = true)))
102+
100103
def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get)
101104

102105
def currentFeerates: FeeratesPerKw = feerates.get()
@@ -604,7 +607,16 @@ object NodeParams extends Logging {
604607
timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS),
605608
maxAttempts = config.getInt("onion-messages.max-attempts"),
606609
),
607-
purgeInvoicesInterval = purgeInvoicesInterval
610+
purgeInvoicesInterval = purgeInvoicesInterval,
611+
liquidityAdsConfig_opt = if (config.getBoolean("liquidity-ads.enabled")) {
612+
Some(LiquidityAds.Config(
613+
feeBase = Satoshi(config.getInt("liquidity-ads.fee-base-satoshis")),
614+
feeProportional = config.getInt("liquidity-ads.fee-basis-points"),
615+
maxLeaseDuration = config.getInt("liquidity-ads.max-duration-blocks"),
616+
))
617+
} else {
618+
None
619+
},
608620
)
609621
}
610622
}

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

Lines changed: 7 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], liquidityRates_opt: Option[LiquidityAds.LeaseRates], timestamp: TimestampSecond = TimestampSecond.now()): 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,10 @@ 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: Set[NodeAnnouncementTlv] = Set(
82+
liquidityRates_opt.map(NodeAnnouncementTlv.LiquidityAdsTlv),
83+
).flatten
84+
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream(tlvs))
8285
val sig = Crypto.sign(witness, nodeSecret)
8386
NodeAnnouncement(
8487
signature = sig,
@@ -87,7 +90,8 @@ object Announcements {
8790
rgbColor = color,
8891
alias = alias,
8992
features = features.unscoped(),
90-
addresses = sortedAddresses
93+
addresses = sortedAddresses,
94+
tlvStream = TlvStream(tlvs)
9195
)
9296
}
9397

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm
9999

100100
// on restart we update our node announcement
101101
// note that if we don't currently have public channels, this will be ignored
102-
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
102+
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), nodeParams.liquidityRates_opt)
103103
self ! nodeAnn
104104

105105
log.info("initialization completed, ready to process messages")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ object Validation {
209209
// in case this was our first local channel, we make a node announcement
210210
if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) {
211211
log.info("first local channel validated, announcing local node")
212-
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
212+
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), nodeParams.liquidityRates_opt)
213213
handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn)
214214
} else d1
215215
}

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees
2323
import fr.acinq.eclair.transactions.Transactions
2424
import fr.acinq.eclair.wire.protocol.CommonCodecs.{blockHeight, millisatoshi32, publicKey, satoshi32}
2525
import fr.acinq.eclair.wire.protocol.TlvCodecs.tmillisatoshi32
26-
import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshi, ToMilliSatoshiConversion}
26+
import fr.acinq.eclair.{BlockHeight, MilliSatoshi, ToMilliSatoshiConversion}
2727
import scodec.Codec
2828
import scodec.bits.ByteVector
2929
import scodec.codecs._
@@ -41,8 +41,13 @@ import java.nio.charset.StandardCharsets
4141
*/
4242
object LiquidityAds {
4343

44-
/** Liquidity leases are valid for a fixed duration, after which they must be renewed. */
45-
val LeaseDuration = CltvExpiryDelta(1008) // 1 week
44+
case class Config(feeBase: Satoshi, feeProportional: Int, maxLeaseDuration: Int) {
45+
def leaseRates(relayFees: RelayFees): LeaseRates = {
46+
// We make the remote node pay for one p2wpkh input and one p2wpkh output.
47+
// If we need more inputs, we will pay the fees for those additional inputs ourselves.
48+
LeaseRates(Transactions.claimP2WPKHOutputWeight, feeProportional, (relayFees.feeProportionalMillionths / 100).toInt, feeBase, relayFees.feeBase)
49+
}
50+
}
4651

4752
/**
4853
* Liquidity is leased using the following rates:
@@ -70,12 +75,6 @@ object LiquidityAds {
7075
}
7176
}
7277

73-
object LeaseRates {
74-
def apply(fundingWeight: Int, leaseFeeBase: Satoshi, leaseFeeProportional: Int, relayFees: RelayFees): LeaseRates = {
75-
LeaseRates(fundingWeight, leaseFeeProportional, (relayFees.feeProportionalMillionths / 100).toInt, leaseFeeBase, relayFees.feeBase)
76-
}
77-
}
78-
7978
val leaseRatesCodec: Codec[LeaseRates] = (
8079
("funding_weight" | uint16) ::
8180
("lease_fee_basis" | uint16) ::

eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, Re
3030
import fr.acinq.eclair.router.Graph.{MessagePath, WeightRatios}
3131
import fr.acinq.eclair.router.PathFindingExperimentConf
3232
import fr.acinq.eclair.router.Router.{MessageRouteParams, MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries}
33-
import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress, OnionRoutingPacket}
33+
import fr.acinq.eclair.wire.protocol.{Color, EncodingType, LiquidityAds, NodeAddress, OnionRoutingPacket}
3434
import org.scalatest.Tag
3535
import scodec.bits.{ByteVector, HexStringSyntax}
3636

@@ -51,6 +51,7 @@ object TestConstants {
5151
val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat)
5252
val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat)
5353
val emptyOnionPacket: OnionRoutingPacket = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes)
54+
val defaultLiquidityRates: LiquidityAds.LeaseRates = LiquidityAds.LeaseRates(500, 100, 10, 100 sat, 200 msat)
5455

5556
case object TestFeature extends Feature with InitFeature with NodeFeature {
5657
val rfcName = "test_feature"
@@ -224,7 +225,8 @@ object TestConstants {
224225
timeout = 200 millis,
225226
maxAttempts = 2,
226227
),
227-
purgeInvoicesInterval = None
228+
purgeInvoicesInterval = None,
229+
liquidityAdsConfig_opt = None,
228230
)
229231

230232
def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams(
@@ -389,7 +391,8 @@ object TestConstants {
389391
timeout = 100 millis,
390392
maxAttempts = 2,
391393
),
392-
purgeInvoicesInterval = None
394+
purgeInvoicesInterval = None,
395+
liquidityAdsConfig_opt = None,
393396
)
394397

395398
def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams(

eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.router.Announcements
2929
import fr.acinq.eclair.router.Router.PublicChannel
3030
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
3131
import fr.acinq.eclair.wire.protocol._
32-
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestDatabases, randomBytes32, randomKey}
32+
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestConstants, TestDatabases, randomBytes32, randomKey}
3333
import org.scalatest.funsuite.AnyFunSuite
3434
import scodec.bits.HexStringSyntax
3535

@@ -56,11 +56,11 @@ class NetworkDbSpec extends AnyFunSuite {
5656
forAllDbs { dbs =>
5757
val db = dbs.network
5858

59-
val node_1 = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty)
60-
val node_2 = Announcements.makeNodeAnnouncement(randomKey(), "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional))
61-
val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional))
62-
val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-eve", Color(100.toByte, 200.toByte, 300.toByte), Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000) :: Nil, Features.empty)
63-
val node_5 = Announcements.makeNodeAnnouncement(randomKey(), "node-frank", Color(100.toByte, 200.toByte, 300.toByte), DnsHostname("eclair.invalid", 42000) :: Nil, Features.empty)
59+
val node_1 = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty, None)
60+
val node_2 = Announcements.makeNodeAnnouncement(randomKey(), "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional), Some(TestConstants.defaultLiquidityRates))
61+
val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional), None)
62+
val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-eve", Color(100.toByte, 200.toByte, 300.toByte), Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000) :: Nil, Features.empty, None)
63+
val node_5 = Announcements.makeNodeAnnouncement(randomKey(), "node-frank", Color(100.toByte, 200.toByte, 300.toByte), DnsHostname("eclair.invalid", 42000) :: Nil, Features.empty, None)
6464

6565
assert(db.listNodes().toSet == Set.empty)
6666
db.addNode(node_1)
@@ -401,7 +401,7 @@ object NetworkDbSpec {
401401
update_2_data_opt: Option[Array[Byte]])
402402

403403
val nodeTestCases: Seq[NodeTestCase] = for (_ <- 0 until 10) yield {
404-
val node = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty)
404+
val node = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty, None)
405405
val data = nodeAnnouncementCodec.encode(node).require.toByteArray
406406
NodeTestCase(
407407
nodeId = node.nodeId,

eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,9 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually
170170
val db = Databases.postgres(baseConfig, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow)
171171
db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
172172
db.channels.updateChannelMeta(ChannelCodecsSpec.normal.channelId, ChannelEvent.EventType.Created)
173-
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-A", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 45.days))
174-
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-B", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 3.days))
175-
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-C", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 7.minutes))
173+
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-A", Color(50, 99, -80), Nil, Features.empty, None, TimestampSecond.now() - 45.days))
174+
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-B", Color(50, 99, -80), Nil, Features.empty, None, TimestampSecond.now() - 3.days))
175+
db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-C", Color(50, 99, -80), Nil, Features.empty, None, TimestampSecond.now() - 7.minutes))
176176
db.audit.add(ChannelPaymentRelayed(421 msat, 400 msat, randomBytes32(), randomBytes32(), randomBytes32(), TimestampMilli.now() - 5.seconds, TimestampMilli.now() - 3.seconds))
177177
db.dataSource.close()
178178
}

0 commit comments

Comments
 (0)