Skip to content

Commit

Permalink
Add recommended_feerates optional message
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Sep 24, 2024
1 parent cfdb088 commit 8f26248
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 6 deletions.
24 changes: 23 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
import fr.acinq.eclair.db._
Expand All @@ -36,6 +36,7 @@ import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf}
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol._
import grizzled.slf4j.Logging
import scodec.bits.ByteVector
Expand Down Expand Up @@ -109,6 +110,27 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,

/** Returns the features that should be used in our init message with the given peer. */
def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures()

/** Returns the feerates we'd like our peer to use when funding channels. */
def recommendedFeerates(remoteNodeId: PublicKey, currentFeerates: FeeratesPerKw, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): RecommendedFeerates = {
val feerateTolerance = onChainFeeConf.feerateToleranceFor(remoteNodeId)
val fundingFeerate = onChainFeeConf.getFundingFeerate(currentFeerates)
val fundingRange = RecommendedFeeratesTlv.FundingFeerateRange(
min = fundingFeerate * feerateTolerance.ratioLow,
max = fundingFeerate * feerateTolerance.ratioHigh,
)
// We use the most likely commitment format, even though there is no guarantee that this is the one that will be used.
val commitmentFormat = ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = false).commitmentFormat
val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentFeerates, remoteNodeId, commitmentFormat, channelConf.minFundingPrivateSatoshis)
val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange(
min = commitmentFeerate * feerateTolerance.ratioLow,
max = commitmentFormat match {
case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh
case _: Transactions.AnchorOutputsCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate)
},
)
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange))
}
}

case class PaymentFinalExpiryConf(min: CltvExpiryDelta, max: CltvExpiryDelta) {
Expand Down
14 changes: 13 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.blockchain.{OnChainChannelFunder, OnchainPubkeyCache}
import fr.acinq.eclair.blockchain.{CurrentFeerates, OnChainChannelFunder, OnchainPubkeyCache}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.io.MessageRelay.Status
Expand Down Expand Up @@ -63,6 +63,8 @@ class Peer(val nodeParams: NodeParams,

import Peer._

context.system.eventStream.subscribe(self, classOf[CurrentFeerates])

startWith(INSTANTIATING, Nothing)

when(INSTANTIATING) {
Expand Down Expand Up @@ -345,6 +347,13 @@ class Peer(val nodeParams: NodeParams,
}
stay()

case Event(current: CurrentFeerates, d) =>
d match {
case d: ConnectedData => d.peerConnection ! nodeParams.recommendedFeerates(remoteNodeId, current.feeratesPerKw, d.localFeatures, d.remoteFeatures)
case _ => ()
}
stay()

case Event(_: Peer.OutgoingMessage, _) => stay() // we got disconnected or reconnected and this message was for the previous connection

case Event(RelayOnionMessage(messageId, _, replyTo_opt), _) =>
Expand Down Expand Up @@ -389,6 +398,9 @@ class Peer(val nodeParams: NodeParams,
// let's bring existing/requested channels online
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)

// We tell our peer what our current feerates are.
connectionReady.peerConnection ! nodeParams.recommendedFeerates(remoteNodeId, nodeParams.currentFeerates, connectionReady.localInit.features, connectionReady.remoteInit.features)

goto(CONNECTED) using ConnectedData(connectionReady.address, connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit, channels)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,12 @@ object LightningMessageCodecs {

//

val recommendedFeeratesCodec: Codec[RecommendedFeerates] = (
("chainHash" | blockHash) ::
("fundingFeerate" | feeratePerKw) ::
("commitmentFeerate" | feeratePerKw) ::
("tlvStream" | RecommendedFeeratesTlv.recommendedFeeratesTlvCodec)).as[RecommendedFeerates]

val unknownMessageCodec: Codec[UnknownMessage] = (
("tag" | uint16) ::
("message" | bytes)
Expand Down Expand Up @@ -479,14 +485,15 @@ object LightningMessageCodecs {
.typecase(513, onionMessageCodec)
// NB: blank lines to minimize merge conflicts

//
//
.typecase(37000, spliceInitCodec)
.typecase(37002, spliceAckCodec)
.typecase(37004, spliceLockedCodec)
//

//
//

//
.typecase(39409, recommendedFeeratesCodec)
//

//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,4 +607,15 @@ case class OnionMessage(blindingKey: PublicKey, onionRoutingPacket: OnionRouting

//

/**
* This message informs our peers of the feerates we recommend using.
* We may reject funding attempts that use values that are too far from our recommended feerates.
*/
case class RecommendedFeerates(chainHash: BlockHash, fundingFeerate: FeeratePerKw, commitmentFeerate: FeeratePerKw, tlvStream: TlvStream[RecommendedFeeratesTlv] = TlvStream.empty) extends SetupMessage with HasChainHash {
val minFundingFeerate: FeeratePerKw = tlvStream.get[RecommendedFeeratesTlv.FundingFeerateRange].map(_.min).getOrElse(fundingFeerate)
val maxFundingFeerate: FeeratePerKw = tlvStream.get[RecommendedFeeratesTlv.FundingFeerateRange].map(_.max).getOrElse(fundingFeerate)
val minCommitmentFeerate: FeeratePerKw = tlvStream.get[RecommendedFeeratesTlv.CommitmentFeerateRange].map(_.min).getOrElse(commitmentFeerate)
val maxCommitmentFeerate: FeeratePerKw = tlvStream.get[RecommendedFeeratesTlv.CommitmentFeerateRange].map(_.max).getOrElse(commitmentFeerate)
}

case class UnknownMessage(tag: Int, data: ByteVector) extends LightningMessage
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.scalacompat.BlockHash
import fr.acinq.eclair.UInt64
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream}
import scodec.Codec
Expand Down Expand Up @@ -85,4 +86,23 @@ sealed trait PongTlv extends Tlv

object PongTlv {
val pongTlvCodec: Codec[TlvStream[PongTlv]] = tlvStream(discriminated[PongTlv].by(varint))
}

sealed trait RecommendedFeeratesTlv extends Tlv

object RecommendedFeeratesTlv {
/** Detailed range of values that will be accepted until the next [[RecommendedFeerates]] message is sent. */
case class FundingFeerateRange(min: FeeratePerKw, max: FeeratePerKw) extends RecommendedFeeratesTlv

private val fundingFeerateRangeCodec: Codec[FundingFeerateRange] = tlvField(feeratePerKw :: feeratePerKw)

/** Detailed range of values that will be accepted until the next [[RecommendedFeerates]] message is sent. */
case class CommitmentFeerateRange(min: FeeratePerKw, max: FeeratePerKw) extends RecommendedFeeratesTlv

private val commitmentFeerateRangeCodec: Codec[CommitmentFeerateRange] = tlvField(feeratePerKw :: feeratePerKw)

val recommendedFeeratesTlvCodec = tlvStream(discriminated[RecommendedFeeratesTlv].by(varint)
.typecase(UInt64(1), fundingFeerateRangeCodec)
.typecase(UInt64(3), commitmentFeerateRangeCodec)
)
}
17 changes: 16 additions & 1 deletion eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.DummyOnChainWallet
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.ChannelStateTestsTags
import fr.acinq.eclair.io.Peer._
Expand Down Expand Up @@ -112,6 +112,7 @@ class PeerSpec extends FixtureSpec {
switchboard.send(peer, Peer.Init(channels))
val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures())
switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, localInit, remoteInit))
peerConnection.expectMsgType[RecommendedFeerates]
val probe = TestProbe()
probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped)))
val peerInfo = probe.expectMsgType[Peer.PeerInfo]
Expand Down Expand Up @@ -282,6 +283,7 @@ class PeerSpec extends FixtureSpec {
}

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

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

test("send recommended feerates when feerate changes") { f =>
import f._

connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal))

// We regularly update our internal feerates.
peer ! CurrentFeerates(FeeratesPerKw(FeeratePerKw(253 sat), FeeratePerKw(1000 sat), FeeratePerKw(2500 sat), FeeratePerKw(5000 sat), FeeratePerKw(10_000 sat)))
val tlvs = TlvStream[RecommendedFeeratesTlv](RecommendedFeeratesTlv.FundingFeerateRange(FeeratePerKw(1250 sat), FeeratePerKw(20_000 sat)), RecommendedFeeratesTlv.CommitmentFeerateRange(FeeratePerKw(2500 sat), FeeratePerKw(40_000 sat)))
val expected = RecommendedFeerates(Block.RegtestGenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(5000 sat), tlvs)
peerConnection.expectMsg(expected)
}

test("don't spawn a channel with duplicate temporary channel id", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,24 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
}
}

test("encode/decode recommended_feerates") {
val fundingRange = RecommendedFeeratesTlv.FundingFeerateRange(FeeratePerKw(5000 sat), FeeratePerKw(15_000 sat))
val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange(FeeratePerKw(253 sat), FeeratePerKw(2_000 sat))
val testCases = Seq(
// @formatter:off
RecommendedFeerates(Block.Testnet3GenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(2500 sat)) -> hex"99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 000009c4 000009c4",
RecommendedFeerates(Block.Testnet3GenesisBlock.hash, FeeratePerKw(5000 sat), FeeratePerKw(253 sat)) -> hex"99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00001388 000000fd",
RecommendedFeerates(Block.Testnet3GenesisBlock.hash, FeeratePerKw(10_000 sat), FeeratePerKw(1000 sat), TlvStream(fundingRange, commitmentRange)) -> hex"99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00002710 000003e8 01080000138800003a98 0308000000fd000007d0"
// @formatter:on
)
for ((expected, encoded) <- testCases) {
val decoded = lightningMessageCodec.decode(encoded.bits).require.value
assert(decoded == expected)
val reEncoded = lightningMessageCodec.encode(decoded).require.bytes
assert(reEncoded == encoded)
}
}

test("unknown messages") {
// Non-standard tag number so this message can only be handled by a codec with a fallback
val unknown = UnknownMessage(tag = 47282, data = ByteVector32.Zeroes.bytes)
Expand Down

0 comments on commit 8f26248

Please sign in to comment.