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 Jun 13, 2024
1 parent 6fc8334 commit 9271c1e
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 3 deletions.
11 changes: 10 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 Down Expand Up @@ -109,6 +109,15 @@ 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 fundingFeerate = onChainFeeConf.getFundingFeerate(currentFeerates)
// 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)
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate)
}
}

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 @@ -344,6 +346,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 @@ -388,6 +397,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,11 @@ object LightningMessageCodecs {

//

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

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

//
.typecase(35025, recommendedFeeratesCodec)
//
.typecase(37000, spliceInitCodec)
.typecase(37002, spliceAckCodec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,4 +601,10 @@ 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) extends SetupMessage with HasChainHash

case class UnknownMessage(tag: Int, data: ByteVector) extends LightningMessage
15 changes: 14 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,16 @@ 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)))
peerConnection.expectMsg(RecommendedFeerates(Block.RegtestGenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(5000 sat)))
}

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

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

test("encode/decode recommended_feerates") {
val testCases = Seq(
RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(2500 sat), FeeratePerKw(2500 sat)) -> hex"88d1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 000009c4 000009c4",
RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(5000 sat), FeeratePerKw(253 sat)) -> hex"88d1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00001388 000000fd",
)
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 9271c1e

Please sign in to comment.