Skip to content

Commit 6ad4d70

Browse files
committed
Add recommended_feerates optional message
We send to our peers an optional message that tells them the feerates we'd like to use for funding channels. This lets them know which values are acceptable to us, in case we reject their funding requests. This is using an odd type and will be automatically ignored by existing nodes who don't support that feature.
1 parent e23a6f5 commit 6ad4d70

File tree

6 files changed

+63
-3
lines changed

6 files changed

+63
-3
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
2222
import fr.acinq.eclair.Setup.Seeds
2323
import fr.acinq.eclair.blockchain.fee._
24-
import fr.acinq.eclair.channel.ChannelFlags
2524
import fr.acinq.eclair.channel.fsm.Channel
2625
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
26+
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
2727
import fr.acinq.eclair.crypto.Noise.KeyPair
2828
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
2929
import fr.acinq.eclair.db._
@@ -109,6 +109,15 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
109109

110110
/** Returns the features that should be used in our init message with the given peer. */
111111
def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures()
112+
113+
/** Returns the feerates we'd like our peer to use when funding channels. */
114+
def recommendedFeerates(remoteNodeId: PublicKey, currentFeerates: FeeratesPerKw, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): RecommendedFeerates = {
115+
val fundingFeerate = onChainFeeConf.getFundingFeerate(currentFeerates)
116+
// We use the most likely commitment format, even though there is no guarantee that this is the one that will be used.
117+
val commitmentFormat = ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = false).commitmentFormat
118+
val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentFeerates, remoteNodeId, commitmentFormat, channelConf.minFundingPrivateSatoshis)
119+
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate)
120+
}
112121
}
113122

114123
case class PaymentFinalExpiryConf(min: CltvExpiryDelta, max: CltvExpiryDelta) {

eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator
2929
import fr.acinq.eclair._
3030
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
3131
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
32-
import fr.acinq.eclair.blockchain.{OnChainChannelFunder, OnchainPubkeyCache}
32+
import fr.acinq.eclair.blockchain.{CurrentFeerates, OnChainChannelFunder, OnchainPubkeyCache}
3333
import fr.acinq.eclair.channel._
3434
import fr.acinq.eclair.channel.fsm.Channel
3535
import fr.acinq.eclair.io.MessageRelay.Status
@@ -63,6 +63,8 @@ class Peer(val nodeParams: NodeParams,
6363

6464
import Peer._
6565

66+
context.system.eventStream.subscribe(self, classOf[CurrentFeerates])
67+
6668
startWith(INSTANTIATING, Nothing)
6769

6870
when(INSTANTIATING) {
@@ -344,6 +346,13 @@ class Peer(val nodeParams: NodeParams,
344346
}
345347
stay()
346348

349+
case Event(current: CurrentFeerates, d) =>
350+
d match {
351+
case d: ConnectedData => d.peerConnection ! nodeParams.recommendedFeerates(remoteNodeId, current.feeratesPerKw, d.localFeatures, d.remoteFeatures)
352+
case _ => ()
353+
}
354+
stay()
355+
347356
case Event(_: Peer.OutgoingMessage, _) => stay() // we got disconnected or reconnected and this message was for the previous connection
348357

349358
case Event(RelayOnionMessage(messageId, _, replyTo_opt), _) =>
@@ -388,6 +397,9 @@ class Peer(val nodeParams: NodeParams,
388397
// let's bring existing/requested channels online
389398
channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id)
390399

400+
// We tell our peer what our current feerates are.
401+
connectionReady.peerConnection ! nodeParams.recommendedFeerates(remoteNodeId, nodeParams.currentFeerates, connectionReady.localInit.features, connectionReady.remoteInit.features)
402+
391403
goto(CONNECTED) using ConnectedData(connectionReady.address, connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit, channels)
392404
}
393405

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ object LightningMessageCodecs {
429429

430430
//
431431

432+
val recommendedFeeratesCodec: Codec[RecommendedFeerates] = (
433+
("chainHash" | blockHash) ::
434+
("fundingFeerate" | feeratePerKw) ::
435+
("commitmentFeerate" | feeratePerKw)).as[RecommendedFeerates]
436+
432437
val unknownMessageCodec: Codec[UnknownMessage] = (
433438
("tag" | uint16) ::
434439
("message" | bytes)
@@ -479,6 +484,8 @@ object LightningMessageCodecs {
479484
.typecase(513, onionMessageCodec)
480485
// NB: blank lines to minimize merge conflicts
481486

487+
//
488+
.typecase(35025, recommendedFeeratesCodec)
482489
//
483490
.typecase(37000, spliceInitCodec)
484491
.typecase(37002, spliceAckCodec)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,4 +601,10 @@ case class OnionMessage(blindingKey: PublicKey, onionRoutingPacket: OnionRouting
601601

602602
//
603603

604+
/**
605+
* This message informs our peers of the feerates we recommend using.
606+
* We may reject funding attempts that use values that are too far from our recommended feerates.
607+
*/
608+
case class RecommendedFeerates(chainHash: BlockHash, fundingFeerate: FeeratePerKw, commitmentFeerate: FeeratePerKw) extends SetupMessage with HasChainHash
609+
604610
case class UnknownMessage(tag: Int, data: ByteVector) extends LightningMessage

eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
2525
import fr.acinq.eclair.Features._
2626
import fr.acinq.eclair.TestConstants._
2727
import fr.acinq.eclair._
28-
import fr.acinq.eclair.blockchain.DummyOnChainWallet
2928
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
29+
import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet}
3030
import fr.acinq.eclair.channel._
3131
import fr.acinq.eclair.channel.states.ChannelStateTestsTags
3232
import fr.acinq.eclair.io.Peer._
@@ -112,6 +112,7 @@ class PeerSpec extends FixtureSpec {
112112
switchboard.send(peer, Peer.Init(channels))
113113
val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures())
114114
switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, localInit, remoteInit))
115+
peerConnection.expectMsgType[RecommendedFeerates]
115116
val probe = TestProbe()
116117
probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped)))
117118
val peerInfo = probe.expectMsgType[Peer.PeerInfo]
@@ -282,6 +283,7 @@ class PeerSpec extends FixtureSpec {
282283
}
283284

284285
peerConnection2.send(peer, PeerConnection.ConnectionReady(peerConnection2.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit))
286+
peerConnection2.expectMsgType[RecommendedFeerates]
285287
// peer should kill previous connection
286288
peerConnection1.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced))
287289
channel.expectMsg(INPUT_DISCONNECTED)
@@ -291,6 +293,7 @@ class PeerSpec extends FixtureSpec {
291293
}
292294

293295
peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit))
296+
peerConnection3.expectMsgType[RecommendedFeerates]
294297
// peer should kill previous connection
295298
peerConnection2.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced))
296299
channel.expectMsg(INPUT_DISCONNECTED)
@@ -325,6 +328,16 @@ class PeerSpec extends FixtureSpec {
325328
monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.CONNECTING, ReconnectionTask.IDLE))
326329
}
327330

331+
test("send recommended feerates when feerate changes") { f =>
332+
import f._
333+
334+
connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal))
335+
336+
// We regularly update our internal feerates.
337+
peer ! CurrentFeerates(FeeratesPerKw(FeeratePerKw(253 sat), FeeratePerKw(1000 sat), FeeratePerKw(2500 sat), FeeratePerKw(5000 sat), FeeratePerKw(10_000 sat)))
338+
peerConnection.expectMsg(RecommendedFeerates(Block.RegtestGenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(5000 sat)))
339+
}
340+
328341
test("don't spawn a channel with duplicate temporary channel id") { f =>
329342
import f._
330343

eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,19 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
488488
}
489489
}
490490

491+
test("encode/decode recommended_feerates") {
492+
val testCases = Seq(
493+
RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(2500 sat)) -> hex"88d1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 000009c4 000009c4",
494+
RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(5000 sat), FeeratePerKw(253 sat)) -> hex"88d1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00001388 000000fd",
495+
)
496+
for ((expected, encoded) <- testCases) {
497+
val decoded = lightningMessageCodec.decode(encoded.bits).require.value
498+
assert(decoded == expected)
499+
val reEncoded = lightningMessageCodec.encode(decoded).require.bytes
500+
assert(reEncoded == encoded)
501+
}
502+
}
503+
491504
test("unknown messages") {
492505
// Non-standard tag number so this message can only be handled by a codec with a fallback
493506
val unknown = UnknownMessage(tag = 47282, data = ByteVector32.Zeroes.bytes)

0 commit comments

Comments
 (0)