1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 10:39:19 +01:00

Enforce a minimum fee rate (#530)

* Enforce a minimum fee rate, with a default at 1 satoshi/byte

Same value as bitcoin core's minimum relay fee

* add `minFeeratePerByte` to `FallbackFeeProvider`

and require that `FeeratesPerByte` and `FeeratesPerKw` be always > 0
This commit is contained in:
Fabrice Drouin 2018-04-03 19:43:37 +02:00 committed by Pierre-Marie Padiou
parent 1f6d180a0c
commit bafa4557dc
13 changed files with 120 additions and 103 deletions

View File

@ -35,6 +35,7 @@ eclair {
72 = 20
}
}
min-feerate = 1 // minimum feerate in satoshis per byte (same default value as bitcoin core's minimum relay fee)
node-alias = "eclair"
node-color = "49daaa"

View File

@ -126,74 +126,83 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}
def bootstrap: Future[Kit] = {
val zmqConnected = Promise[Boolean]()
val tcpBound = Promise[Unit]()
val defaultFeerates = FeeratesPerByte(block_1 = config.getLong("default-feerates.delay-blocks.1"), blocks_2 = config.getLong("default-feerates.delay-blocks.2"), blocks_6 = config.getLong("default-feerates.delay-blocks.6"), blocks_12 = config.getLong("default-feerates.delay-blocks.12"), blocks_36 = config.getLong("default-feerates.delay-blocks.36"), blocks_72 = config.getLong("default-feerates.delay-blocks.72"))
Globals.feeratesPerByte.set(defaultFeerates)
Globals.feeratesPerKw.set(FeeratesPerKw(defaultFeerates))
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
val feeProvider = (nodeParams.chainHash, bitcoin) match {
case (Block.RegtestGenesisBlock.hash, _) => new ConstantFeeProvider(defaultFeerates)
case (_, Bitcoind(bitcoinClient)) => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case _ => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
}
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerByte =>
Globals.feeratesPerByte.set(feerates)
Globals.feeratesPerKw.set(FeeratesPerKw(feerates))
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
})
val watcher = bitcoin match {
case Bitcoind(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoinClient))), "watcher", SupervisorStrategy.Resume))
case Electrum(electrumClient) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
}
val wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient)
case Electrum(electrumClient) =>
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash)), "electrum-wallet")
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
}
wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
}
val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => LocalPaymentHandler.props(nodeParams)
case "noop" => Props[NoopPaymentHandler]
}, "payment-handler", SupervisorStrategy.Resume))
val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume))
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
val authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume))
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart))
val kit = Kit(
nodeParams = nodeParams,
system = system,
watcher = watcher,
paymentHandler = paymentHandler,
register = register,
relayer = relayer,
router = router,
switchboard = switchboard,
paymentInitiator = paymentInitiator,
server = server,
wallet = wallet)
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
for {
_ <- Future.successful(true)
feeratesRetrieved = Promise[Boolean]()
zmqConnected = Promise[Boolean]()
tcpBound = Promise[Unit]()
defaultFeerates = FeeratesPerByte(
block_1 = config.getLong("default-feerates.delay-blocks.1"),
blocks_2 = config.getLong("default-feerates.delay-blocks.2"),
blocks_6 = config.getLong("default-feerates.delay-blocks.6"),
blocks_12 = config.getLong("default-feerates.delay-blocks.12"),
blocks_36 = config.getLong("default-feerates.delay-blocks.36"),
blocks_72 = config.getLong("default-feerates.delay-blocks.72")
)
minFeeratePerByte = config.getLong("min-feerate")
feeProvider = (nodeParams.chainHash, bitcoin) match {
case (Block.RegtestGenesisBlock.hash, _) => new ConstantFeeProvider(defaultFeerates)
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!
}
_ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerByte =>
Globals.feeratesPerByte.set(feerates)
Globals.feeratesPerKw.set(FeeratesPerKw(feerates))
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
feeratesRetrieved.trySuccess(true)
})
_ <- feeratesRetrieved.future
watcher = bitcoin match {
case Bitcoind(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoinClient))), "watcher", SupervisorStrategy.Resume))
case Electrum(electrumClient) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
}
wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient)
case Electrum(electrumClient) =>
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash)), "electrum-wallet")
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
}
_ = wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
}
paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => LocalPaymentHandler.props(nodeParams)
case "noop" => Props[NoopPaymentHandler]
}, "payment-handler", SupervisorStrategy.Resume))
register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume))
router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume))
switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart))
kit = Kit(
nodeParams = nodeParams,
system = system,
watcher = watcher,
paymentHandler = paymentHandler,
register = register,
relayer = relayer,
router = router,
switchboard = switchboard,
paymentInitiator = paymentInitiator,
server = server,
wallet = wallet)
zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- if (config.getBoolean("api.enabled")) {

View File

@ -23,6 +23,7 @@ import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.eclair.feerateKbToByte
import org.json4s.JsonAST.{JInt, JValue}
import org.json4s.{DefaultFormats, jackson}
@ -58,7 +59,7 @@ object BitgoFeeProvider {
val blockTargets = json \ "feeByBlockTarget"
blockTargets.foldField(Seq.empty[BlockTarget]) {
// we divide by 1024 because bitgo returns estimates in Satoshi/Kb and we use estimates in Satoshi/Byte
case (list, (strBlockTarget, JInt(feePerKb))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKb.longValue() / 1024)
case (list, (strBlockTarget, JInt(feePerKb))) => list :+ BlockTarget(strBlockTarget.toInt, feerateKbToByte(feePerKb.longValue()))
}
}

View File

@ -66,8 +66,8 @@ object EarnDotComFeeProvider {
def extractFeerate(feeRanges: Seq[FeeRange], maxBlockDelay: Int): Long = {
// first we keep only fee ranges with a max block delay below the limit
val belowLimit = feeRanges.filter(_.maxDelay <= maxBlockDelay)
// out of all the remaining fee ranges, we select the one with the minimum higher bound
belowLimit.minBy(_.maxFee).maxFee
// out of all the remaining fee ranges, we select the one with the minimum higher bound and make sure it is > 0
Math.max(belowLimit.minBy(_.maxFee).maxFee, 1)
}
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerByte =

View File

@ -20,10 +20,14 @@ import scala.concurrent.{ExecutionContext, Future}
/**
* This provider will try all child providers in sequence, until one of them works
*
* @param providers a sequence of providers; they will be tried one after the others until one of them succeeds
* @param minFeeratePerByte a configurable minimum value for feerates
*/
class FallbackFeeProvider(providers: Seq[FeeProvider])(implicit ec: ExecutionContext) extends FeeProvider {
class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: Long)(implicit ec: ExecutionContext) extends FeeProvider {
require(providers.size >= 1, "need at least one fee provider")
require(minFeeratePerByte > 0, "minimum fee rate must be strictly greater than 0")
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerByte] =
fallbacks match {
@ -31,6 +35,19 @@ class FallbackFeeProvider(providers: Seq[FeeProvider])(implicit ec: ExecutionCon
case head +: remaining => head.getFeerates.recoverWith { case _ => getFeerates(remaining) }
}
override def getFeerates: Future[FeeratesPerByte] = getFeerates(providers)
override def getFeerates: Future[FeeratesPerByte] = getFeerates(providers).map(FallbackFeeProvider.enforceMinimumFeerate(_, minFeeratePerByte))
}
object FallbackFeeProvider {
def enforceMinimumFeerate(feeratesPerByte: FeeratesPerByte, minFeeratePerByte: Long) : FeeratesPerByte = feeratesPerByte.copy(
block_1 = Math.max(feeratesPerByte.block_1, minFeeratePerByte),
blocks_2 = Math.max(feeratesPerByte.blocks_2, minFeeratePerByte),
blocks_6 = Math.max(feeratesPerByte.blocks_6, minFeeratePerByte),
blocks_12 = Math.max(feeratesPerByte.blocks_12, minFeeratePerByte),
blocks_36 = Math.max(feeratesPerByte.blocks_36, minFeeratePerByte),
blocks_72 = Math.max(feeratesPerByte.blocks_72, minFeeratePerByte)
)
}

View File

@ -29,9 +29,13 @@ trait FeeProvider {
}
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
}
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
}
object FeeratesPerKw {
def apply(feerates: FeeratesPerByte): FeeratesPerKw = FeeratesPerKw(

View File

@ -96,8 +96,7 @@ object Helpers {
Math.abs((2.0 * (remoteFeeratePerKw - localFeeratePerKw)) / (localFeeratePerKw + remoteFeeratePerKw))
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
// negative feerate can happen in regtest mode
networkFeeratePerKw > 0 && feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
/**
*
@ -107,10 +106,8 @@ object Helpers {
* @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
}
def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long, maxFeerateMismatchRatio: Double): Boolean =
feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = {
val features = BinaryData.empty // empty features for now

View File

@ -51,6 +51,8 @@ package object eclair {
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
}
def feerateKbToByte(feeratePerKb: Long): Long = Math.max(feeratePerKb / 1024, 1)
/**
* Converts feerate in satoshi-per-bytes to feerate in satoshi-per-kw
*

View File

@ -44,7 +44,7 @@ abstract class TestkitBaseClass extends TestKit(ActorSystem("test")) with fixtur
override def afterAll {
TestKit.shutdownActorSystem(system)
Globals.feeratesPerKw.set(FeeratesPerKw.single(0))
Globals.feeratesPerKw.set(FeeratesPerKw.single(1))
}
}

View File

@ -56,7 +56,7 @@ class FallbackFeeProviderSpec extends FunSuite {
val provider5 = new FailingFeeProvider(5, dummyFeerates) // fails after 5 tries
val provider7 = new FailingFeeProvider(Int.MaxValue, dummyFeerates) // "never" fails
val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil)
val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil, 1)
assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerByte)
@ -74,5 +74,11 @@ class FallbackFeeProviderSpec extends FunSuite {
}
test("ensure minimum feerate") {
val constantFeeProvider = new ConstantFeeProvider(FeeratesPerByte(1, 1, 1, 1, 1, 1))
val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, 2)
assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerByte(2, 2, 2, 2, 2, 2))
}
}

View File

@ -1576,16 +1576,6 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CurrentFeerate (ignore negative feerate)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
// this happens when in regtest mode
val event = CurrentFeerates(FeeratesPerKw.single(-1))
sender.send(alice, event)
alice2bob.expectNoMsg(500 millis)
}
}
test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()

View File

@ -616,16 +616,6 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CurrentFeerate (ignore negative feerate)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
// this happens when in regtest mode
val event = CurrentFeerates(FeeratesPerKw.single(-1))
sender.send(alice, event)
alice2bob.expectNoMsg(500 millis)
}
}
test("recv BITCOIN_FUNDING_SPENT (their commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
// bob publishes his current commit tx, which contains two pending htlcs alice->bob

View File

@ -83,7 +83,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix
}
override def afterAll {
Globals.feeratesPerKw.set(FeeratesPerKw.single(0))
Globals.feeratesPerKw.set(FeeratesPerKw.single(1))
TestKit.shutdownActorSystem(system)
}