mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-20 10:39:19 +01:00
make max-feerate-mismatch configurable and set default value to 500% (#62)
* make max-feerate-mismatch configurable and set default value to 500% there can be a significant gap between the fee rate estimated by different bitcoin clients (see estimatesmartfee) so we must set a reasonably high value for the threshold above which we consider htat local and remote fee rates are too different and close the channel. * use difference/average to compare feerate it becomes symmetrical and easier to reason with, and also more forgiving. * make "UpdateFee" minimum ratio configurable
This commit is contained in:
parent
e2350ca8b1
commit
fd56d35073
@ -38,6 +38,14 @@ eclair {
|
||||
fee-base-msat = 546000
|
||||
fee-proportional-millionth = 10
|
||||
|
||||
// maximum local vs remote feerate mismatch; 1.0 means 100%
|
||||
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch
|
||||
max-feerate-mismatch = 1.5
|
||||
|
||||
// funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater
|
||||
// than this ratio.
|
||||
update-fee_min-diff-ratio = 0.1
|
||||
|
||||
router-broadcast-interval = 10 seconds // this should be 60 seconds on mainnet
|
||||
router-validate-interval = 2 seconds // this should be high enough to have a decent level of parallelism
|
||||
|
||||
|
@ -21,22 +21,6 @@ object Globals {
|
||||
* The value is updated by the [[fr.acinq.eclair.blockchain.PeerWatcher]] and read by all actors, hence it needs to be thread-safe.
|
||||
*/
|
||||
val feeratePerKw = new AtomicLong(0)
|
||||
|
||||
object Constants {
|
||||
|
||||
/**
|
||||
* Funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater
|
||||
* than this ratio.
|
||||
*/
|
||||
val UPDATE_FEE_MIN_DIFF_RATIO = 0.1
|
||||
|
||||
/**
|
||||
* Fundee will unilaterally close the channel if the difference between the feeratePerKw proposed by the funder in an UpdateFee and
|
||||
* actual current network fee is greater than this ratio.
|
||||
*/
|
||||
val UPDATE_FEE_MAX_DIFF_RATIO = 0.3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,7 +43,9 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
|
||||
announcementsDb: SimpleTypedDb[String, LightningMessage],
|
||||
routerBroadcastInterval: FiniteDuration,
|
||||
routerValidateInterval: FiniteDuration,
|
||||
pingInterval: FiniteDuration)
|
||||
pingInterval: FiniteDuration,
|
||||
maxFeerateMismatch: Double,
|
||||
updateFeeMinDiffRatio: Double)
|
||||
|
||||
object NodeParams {
|
||||
|
||||
@ -102,6 +104,8 @@ object NodeParams {
|
||||
announcementsDb = Dbs.makeAnnouncementDb(db),
|
||||
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS),
|
||||
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval").getSeconds, TimeUnit.SECONDS),
|
||||
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS))
|
||||
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
|
||||
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),
|
||||
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"))
|
||||
}
|
||||
}
|
||||
|
@ -597,10 +597,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
||||
|
||||
case Event(CurrentFeerate(feeratePerKw), d: DATA_NORMAL) =>
|
||||
d.commitments.localParams.isFunder match {
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw) =>
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
|
||||
self ! CMD_UPDATE_FEE(feeratePerKw, commit = true)
|
||||
stay
|
||||
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw) =>
|
||||
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw, nodeParams.maxFeerateMismatch) =>
|
||||
handleLocalError(new RuntimeException(s"local/remote feerates are too different: remoteFeeratePerKw=${d.commitments.localCommit.spec.feeratePerKw} localFeeratePerKw=$feeratePerKw"), d)
|
||||
case _ => stay
|
||||
}
|
||||
@ -791,10 +791,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
||||
|
||||
case Event(CurrentFeerate(feeratePerKw), d: DATA_SHUTDOWN) =>
|
||||
d.commitments.localParams.isFunder match {
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw) =>
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
|
||||
self ! CMD_UPDATE_FEE(feeratePerKw, commit = true)
|
||||
stay
|
||||
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw) =>
|
||||
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, feeratePerKw, nodeParams.maxFeerateMismatch) =>
|
||||
handleLocalError(new RuntimeException(s"local/remote feerates are too different: remoteFeeratePerKw=${d.commitments.localCommit.spec.feeratePerKw} localFeeratePerKw=$feeratePerKw"), d)
|
||||
case _ => stay
|
||||
}
|
||||
|
@ -167,7 +167,8 @@ final case class LocalParams(nodeId: PublicKey,
|
||||
shaSeed: BinaryData,
|
||||
isFunder: Boolean,
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData)
|
||||
localFeatures: BinaryData,
|
||||
maxFeerateMismatch: Double)
|
||||
|
||||
final case class RemoteParams(nodeId: PublicKey,
|
||||
dustLimitSatoshis: Long,
|
||||
|
@ -290,7 +290,7 @@ object Commitments extends Logging {
|
||||
}
|
||||
|
||||
val localFeeratePerKw = Globals.feeratePerKw.get()
|
||||
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw)) {
|
||||
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, commitments.localParams.maxFeerateMismatch)) {
|
||||
throw new RuntimeException(s"local/remote feerates are too different: remoteFeeratePerKw=${fee.feeratePerKw} localFeeratePerKw=$localFeeratePerKw")
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ package fr.acinq.eclair.channel
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256}
|
||||
import fr.acinq.bitcoin.Script._
|
||||
import fr.acinq.bitcoin.{OutPoint, _}
|
||||
import fr.acinq.eclair.Globals.Constants.{UPDATE_FEE_MAX_DIFF_RATIO, UPDATE_FEE_MIN_DIFF_RATIO}
|
||||
import fr.acinq.eclair.crypto.Generators
|
||||
import fr.acinq.eclair.transactions.Scripts._
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
@ -65,13 +64,31 @@ object Helpers {
|
||||
}
|
||||
}
|
||||
|
||||
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long): Boolean =
|
||||
// negative feerate can happen in regtest mode
|
||||
networkFeeratePerKw > 0 && Math.abs((networkFeeratePerKw - commitmentFeeratePerKw) / commitmentFeeratePerKw.toDouble) > UPDATE_FEE_MIN_DIFF_RATIO
|
||||
/**
|
||||
*
|
||||
* @param remoteFeeratePerKw remote fee rate per kiloweight
|
||||
* @param localFeeratePerKw local fee rate per kiloweight
|
||||
* @return the "normalized" difference between local and remote fee rate, i.e. |remote - local| / avg(local, remote)
|
||||
*/
|
||||
def feeRateMismatch(remoteFeeratePerKw: Long, localFeeratePerKw: Long): Double =
|
||||
Math.abs((2.0 * (remoteFeeratePerKw - localFeeratePerKw)) / (localFeeratePerKw + remoteFeeratePerKw))
|
||||
|
||||
def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long): Boolean =
|
||||
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
|
||||
// negative feerate can happen in regtest mode
|
||||
remoteFeeratePerKw > 0 && Math.abs((remoteFeeratePerKw - localFeeratePerKw) / localFeeratePerKw.toDouble) > UPDATE_FEE_MAX_DIFF_RATIO
|
||||
networkFeeratePerKw > 0 && feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
|
||||
|
||||
/**
|
||||
*
|
||||
* @param remoteFeeratePerKw remote fee rate per kiloweight
|
||||
* @param localFeeratePerKw local fee rate per kiloweight
|
||||
* @param maxFeerateMismatchRatio maximum fee rate mismatch ratio
|
||||
* @return true if the difference between local and remote fee rates is too high.
|
||||
* the actual check is |remote - local| / avg(local, remote) > mismatch ratio
|
||||
*/
|
||||
def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = {
|
||||
// negative feerate can happen in regtest mode
|
||||
remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio
|
||||
}
|
||||
|
||||
object Funding {
|
||||
|
||||
@ -103,7 +120,7 @@ object Helpers {
|
||||
if (!localParams.isFunder) {
|
||||
// they are funder, we need to make sure that they can pay the fee is reasonable, and that they can afford to pay it
|
||||
val localFeeratePerKw = Globals.feeratePerKw.get()
|
||||
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw)) {
|
||||
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw, localParams.maxFeerateMismatch)) {
|
||||
throw new RuntimeException(s"local/remote feerates are too different: remoteFeeratePerKw=$initialFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
}
|
||||
val toRemote = MilliSatoshi(remoteSpec.toLocalMsat)
|
||||
|
@ -257,7 +257,8 @@ object Peer {
|
||||
shaSeed = Crypto.sha256(generateKey(nodeParams, keyIndex :: 4L :: Nil).toBin), // TODO: check that
|
||||
isFunder = isFunder,
|
||||
globalFeatures = nodeParams.globalFeatures,
|
||||
localFeatures = nodeParams.localFeatures)
|
||||
localFeatures = nodeParams.localFeatures,
|
||||
maxFeerateMismatch = nodeParams.maxFeerateMismatch)
|
||||
}
|
||||
|
||||
def randomTemporaryChannelId: BinaryData = {
|
||||
|
@ -46,7 +46,9 @@ object TestConstants {
|
||||
announcementsDb = Dbs.makeAnnouncementDb(db),
|
||||
routerBroadcastInterval = 60 seconds,
|
||||
routerValidateInterval = 2 seconds,
|
||||
pingInterval = 30 seconds)
|
||||
pingInterval = 30 seconds,
|
||||
maxFeerateMismatch = 1.5,
|
||||
updateFeeMinDiffRatio = 0.1)
|
||||
val id = nodeParams.privateKey.publicKey
|
||||
val channelParams = Peer.makeChannelParams(
|
||||
nodeParams = nodeParams,
|
||||
@ -87,7 +89,9 @@ object TestConstants {
|
||||
announcementsDb = Dbs.makeAnnouncementDb(db),
|
||||
routerBroadcastInterval = 60 seconds,
|
||||
routerValidateInterval = 2 seconds,
|
||||
pingInterval = 30 seconds)
|
||||
pingInterval = 30 seconds,
|
||||
maxFeerateMismatch = 1.0,
|
||||
updateFeeMinDiffRatio = 0.1)
|
||||
val id = nodeParams.privateKey.publicKey
|
||||
val channelParams = Peer.makeChannelParams(
|
||||
nodeParams = nodeParams,
|
||||
|
@ -950,9 +950,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val sender = TestProbe()
|
||||
sender.send(bob, UpdateFee("00" * 32, 50000))
|
||||
sender.send(bob, UpdateFee("00" * 32, 85000))
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=50000 localFeeratePerKw=10000")
|
||||
assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000")
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
bob2blockchain.expectMsg(PublishAsap(tx))
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
@ -1169,7 +1169,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val event = CurrentFeerate(20000)
|
||||
val event = CurrentFeerate(100)
|
||||
sender.send(bob, event)
|
||||
bob2alice.expectMsgType[Error]
|
||||
bob2blockchain.expectMsgType[PublishAsap]
|
||||
|
@ -439,9 +439,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val sender = TestProbe()
|
||||
sender.send(bob, UpdateFee("00" * 32, 50000))
|
||||
sender.send(bob, UpdateFee("00" * 32, 65000))
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=50000 localFeeratePerKw=10000")
|
||||
assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000")
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
bob2blockchain.expectMsg(PublishAsap(tx))
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
@ -502,7 +502,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { case (_, bob, _, bob2alice, _, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val event = CurrentFeerate(20000)
|
||||
val event = CurrentFeerate(1000)
|
||||
sender.send(bob, event)
|
||||
bob2alice.expectMsgType[Error]
|
||||
bob2blockchain.expectMsgType[PublishAsap]
|
||||
|
@ -46,7 +46,8 @@ object ChannelStateSpec {
|
||||
shaSeed = BinaryData("05" * 32),
|
||||
isFunder = true,
|
||||
globalFeatures = "foo".getBytes(),
|
||||
localFeatures = "bar".getBytes())
|
||||
localFeatures = "bar".getBytes(),
|
||||
maxFeerateMismatch = 5.0)
|
||||
|
||||
val remoteParams = RemoteParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
|
Loading…
Reference in New Issue
Block a user