Skip to content

Commit

Permalink
Add a "smooth" fee provider (#684)
Browse files Browse the repository at this point in the history
* Add a "smooth" fee provider

It will return the moving average of fee estimates provided by
its internal fee provider, within a user defined window that can
be set with eclair.smooth-feerate-window. 

* Use a default fee rate smoothing window of 3

This should smooth our fee rate when there is a sudden change in
onchain fees and prevent channels with c-lightning node from getting
closed because they disagree with our fee rate.
  • Loading branch information
sstone committed Aug 30, 2018
1 parent 4b6a7c0 commit f0f8f0c
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 5 deletions.
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Expand Up @@ -36,6 +36,7 @@ eclair {
}
}
min-feerate = 2 // minimum feerate in satoshis per byte
smooth-feerate-window = 3 // 1 = no smoothing

node-alias = "eclair"
node-color = "49daaa"
Expand Down
7 changes: 5 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Expand Up @@ -146,10 +146,13 @@ class Setup(datadir: File,
blocks_72 = config.getLong("default-feerates.delay-blocks.72")
)
minFeeratePerByte = config.getLong("min-feerate")
smoothFeerateWindow = config.getInt("smooth-feerate-window")
feeProvider = (nodeParams.chainHash, bitcoin) match {
case (Block.RegtestGenesisBlock.hash, _) => new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte)
case (_, Bitcoind(bitcoinClient)) => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters!
case _ => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters!
case (_, Bitcoind(bitcoinClient)) =>
new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters!
case _ =>
new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters!
}
_ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerKB =>
Expand Down
@@ -0,0 +1,35 @@
package fr.acinq.eclair.blockchain.fee

import scala.concurrent.{ExecutionContext, Future}

class SmoothFeeProvider(provider: FeeProvider, windowSize: Int)(implicit ec: ExecutionContext) extends FeeProvider {
require(windowSize > 0)

var queue = List.empty[FeeratesPerKB]

def append(rate: FeeratesPerKB): Unit = synchronized {
queue = queue :+ rate
if (queue.length > windowSize) queue = queue.drop(1)
}

override def getFeerates: Future[FeeratesPerKB] = {
for {
rate <- provider.getFeerates
_ = append(rate)
} yield SmoothFeeProvider.smooth(queue)
}
}

object SmoothFeeProvider {

def avg(i: Seq[Long]): Long = i.sum / i.size

def smooth(rates: Seq[FeeratesPerKB]): FeeratesPerKB =
FeeratesPerKB(
block_1 = avg(rates.map(_.block_1)),
blocks_2 = avg(rates.map(_.blocks_2)),
blocks_6 = avg(rates.map(_.blocks_6)),
blocks_12 = avg(rates.map(_.blocks_12)),
blocks_36 = avg(rates.map(_.blocks_36)),
blocks_72 = avg(rates.map(_.blocks_72)))
}
Expand Up @@ -67,7 +67,7 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco
port = config.getInt("bitcoind.rpcport"))

// the regtest client doesn't have enough data to estimate fees yet, so it's suppose to fail
val regtestProvider = new BitcoinCoreFeeProvider(bitcoinClient, FeeratesPerKB(1,2,3,4,5,6))
val regtestProvider = new BitcoinCoreFeeProvider(bitcoinClient, FeeratesPerKB(1, 2, 3, 4, 5, 6))
val sender = TestProbe()
regtestProvider.getFeerates.pipeTo(sender.ref)
assert(sender.expectMsgType[Failure].cause.asInstanceOf[RuntimeException].getMessage.contains("Insufficient data or no feerate found"))
Expand Down Expand Up @@ -103,8 +103,8 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco
}
}

val mockPrivider = new BitcoinCoreFeeProvider(mockBitcoinClient, FeeratesPerKB(1,2,3,4,5,6))
mockPrivider.getFeerates.pipeTo(sender.ref)
val mockProvider = new BitcoinCoreFeeProvider(mockBitcoinClient, FeeratesPerKB(1, 2, 3, 4, 5, 6))
mockProvider.getFeerates.pipeTo(sender.ref)
assert(sender.expectMsgType[FeeratesPerKB] == ref)
}

Expand Down
@@ -0,0 +1,48 @@
package fr.acinq.eclair.blockchain.fee

import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner

import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

@RunWith(classOf[JUnitRunner])
class SmoothFeeProviderSpec extends FunSuite {
test("smooth fee rates") {
val rates = Array(
FeeratesPerKB(100, 200, 300, 400, 500, 600),
FeeratesPerKB(200, 300, 400, 500, 600, 700),
FeeratesPerKB(300, 400, 500, 600, 700, 800),
FeeratesPerKB(300, 400, 500, 600, 700, 800),
FeeratesPerKB(300, 400, 500, 600, 700, 800)
)
val provider = new FeeProvider {
var index = 0

override def getFeerates: Future[FeeratesPerKB] = {
val rate = rates(index)
index = (index + 1) % rates.length
Future.successful(rate)
}
}

val smoothProvider = new SmoothFeeProvider(provider, windowSize = 3)
val f = for {
rate1 <- smoothProvider.getFeerates
rate2 <- smoothProvider.getFeerates
rate3 <- smoothProvider.getFeerates
rate4 <- smoothProvider.getFeerates
rate5 <- smoothProvider.getFeerates
} yield (rate1, rate2, rate3, rate4, rate5)

val (rate1, rate2, rate3, rate4, rate5) = Await.result(f, 5 seconds)
assert(rate1 == rates(0))
assert(rate2 == SmoothFeeProvider.smooth(Seq(rates(0), rates(1))))
assert(rate3 == SmoothFeeProvider.smooth(Seq(rates(0), rates(1), rates(2))))
assert(rate3 == FeeratesPerKB(200, 300, 400, 500, 600, 700))
assert(rate4 == SmoothFeeProvider.smooth(Seq(rates(1), rates(2), rates(3))))
assert(rate5 == rates(4)) // since the last 3 values are the same
}
}

0 comments on commit f0f8f0c

Please sign in to comment.