From 12005af044a1ef126525430bd97bf28bc47c4bad Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Jul 2019 12:27:44 +0200 Subject: [PATCH 01/11] Add FeeConf to NodeParams, add config block for fee configuratio, remove unused 'smartfeeNBlocks' --- eclair-core/src/main/resources/reference.conf | 9 +++++++++ .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 13 +++++++++---- .../fr/acinq/eclair/transactions/Transactions.scala | 1 + .../test/scala/fr/acinq/eclair/TestConstants.scala | 6 ++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index b2de25ee7c..8d984b4704 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -39,6 +39,15 @@ eclair { 72 = 20000 } } + + # number of blocks to target when computing fees for each transaction type + # possible values are: 1, 2, 6, 12, 36, 72 + fee-targets { + funding = 6 + commitment = 2 + mutual-close = 12 + } + min-feerate = 3 // minimum feerate in satoshis per byte smooth-feerate-window = 6 // 1 = no smoothing diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 9266d701e7..e667d75ad2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -20,7 +20,6 @@ import java.io.File import java.net.InetSocketAddress import java.nio.file.Files import java.util.concurrent.TimeUnit - import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32} @@ -30,9 +29,9 @@ import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams +import fr.acinq.eclair.transactions.Transactions.FeeConf import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector - import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -54,7 +53,6 @@ case class NodeParams(keyManager: KeyManager, toRemoteDelayBlocks: Int, maxToLocalDelayBlocks: Int, minDepthBlocks: Int, - smartfeeNBlocks: Int, feeBaseMsat: Int, feeProportionalMillionth: Int, reserveToFundingRatio: Double, @@ -64,6 +62,7 @@ case class NodeParams(keyManager: KeyManager, pingInterval: FiniteDuration, pingTimeout: FiniteDuration, pingDisconnect: Boolean, + feeTargets: FeeConf, maxFeerateMismatch: Double, updateFeeMinDiffRatio: Double, autoReconnect: Boolean, @@ -176,6 +175,12 @@ object NodeParams { .toList .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt + val feeTargets = FeeConf( + fundingBlockTarget = config.getInt("fee-targets.funding"), + commitmentBlockTarget = config.getInt("fee-targets.commitment"), + mutualCloseBlockTarget = config.getInt("fee-targets.mutual-close") + ) + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -192,7 +197,6 @@ object NodeParams { toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), minDepthBlocks = config.getInt("mindepth-blocks"), - smartfeeNBlocks = 3, feeBaseMsat = config.getInt("fee-base-msat"), feeProportionalMillionth = config.getInt("fee-proportional-millionths"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), @@ -202,6 +206,7 @@ object NodeParams { pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS), pingTimeout = FiniteDuration(config.getDuration("ping-timeout").getSeconds, TimeUnit.SECONDS), pingDisconnect = config.getBoolean("ping-disconnect"), + feeTargets = feeTargets, maxFeerateMismatch = config.getDouble("max-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"), autoReconnect = config.getBoolean("auto-reconnect"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 0febeaab9e..894b45e979 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -65,6 +65,7 @@ object Transactions { case object OutputNotFound extends RuntimeException(s"output not found (probably trimmed)") with TxGenerationSkipped case object AmountBelowDustLimit extends RuntimeException(s"amount is below dust limit") with TxGenerationSkipped + case class FeeConf(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int) // @formatter:on /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 79770b5833..ebbe469200 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -26,8 +26,10 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf +import fr.acinq.eclair.transactions.Transactions.FeeConf import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector + import scala.concurrent.duration._ /** @@ -64,7 +66,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -74,6 +75,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, + feeTargets = FeeConf(3, 3, 3), maxFeerateMismatch = 1.5, updateFeeMinDiffRatio = 0.1, autoReconnect = false, @@ -130,7 +132,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -140,6 +141,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, + feeTargets = FeeConf(3, 3, 3), maxFeerateMismatch = 1.0, updateFeeMinDiffRatio = 0.1, autoReconnect = false, From 381c07f7c413852825ef452957b7d999f9d789f3 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jul 2019 10:22:40 +0200 Subject: [PATCH 02/11] Use a custom block target for commitment transaction --- eclair-core/src/main/resources/reference.conf | 9 ++++----- .../eclair/blockchain/fee/FeeProvider.scala | 11 +++++++++++ .../scala/fr/acinq/eclair/channel/Channel.scala | 10 +++++----- .../fr/acinq/eclair/channel/Commitments.scala | 4 ++-- .../scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 4 ++-- .../channel/states/e/NormalStateSpec.scala | 17 ++++++++++++----- .../channel/states/f/ShutdownStateSpec.scala | 7 ++++--- 9 files changed, 42 insertions(+), 24 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8d984b4704..75a2935faa 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -40,12 +40,11 @@ eclair { } } - # number of blocks to target when computing fees for each transaction type - # possible values are: 1, 2, 6, 12, 36, 72 + // number of blocks to target when computing fees for each transaction type, possible values are: 1, 2, 6, 12, 36, 72 fee-targets { - funding = 6 - commitment = 2 - mutual-close = 12 + funding = 6 // confirmation block target for the funding transaction + commitment = 2 // confirmation block target for the commitment transaction (used in force-close scenario) + mutual-close = 12 // confirmation block target for the mutual close transaction } min-feerate = 3 // minimum feerate in satoshis per byte diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 050b7f0a26..fe71fa1a1a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -37,6 +37,17 @@ case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_1 // stores fee rate in satoshi/kw (1 kw = 1000 weight units) 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") + + def getRatePerBlock(blockTarget: Int): Long = blockTarget match { + case 1 => block_1 + case 2 => blocks_2 + case 6 => blocks_6 + case 12 => blocks_12 + case 36 => blocks_36 + case 72 => blocks_72 + case _ => throw new IllegalArgumentException(s"can't choose ratePerBlock=$blockTarget") + } + } object FeeratesPerKw { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 8f71a13f53..97f8bec288 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -681,7 +681,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case Event(fee: UpdateFee, d: DATA_NORMAL) => - Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match { + Try(Commitments.receiveFee(nodeParams.feeTargets, d.commitments, fee, nodeParams.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -853,7 +853,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) => - val networkFeeratePerKw = feeratesPerKw.blocks_2 + val networkFeeratePerKw = feeratesPerKw.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) @@ -1040,7 +1040,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case Event(fee: UpdateFee, d: DATA_SHUTDOWN) => - Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match { + Try(Commitments.receiveFee(nodeParams.feeTargets, d.commitments, fee, nodeParams.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -1135,8 +1135,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) - case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => - val networkFeeratePerKw = feerates.blocks_2 + case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_SHUTDOWN) => + val networkFeeratePerKw = feeratesPerKw.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index fd044ee1c8..42119432a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -310,7 +310,7 @@ object Commitments { (commitments1, fee) } - def receiveFee(commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { + def receiveFee(feeTargets: FeeConf, commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { if (commitments.localParams.isFunder) { throw FundeeCannotSendUpdateFee(commitments.channelId) } @@ -319,7 +319,7 @@ object Commitments { throw FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val localFeeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.commitmentBlockTarget) if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) { throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index f4609344f5..c98b9a5965 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -111,7 +111,7 @@ object Helpers { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val localFeeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) // only enforce dust limit check on mainnet if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 08969aed8d..4264372f51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -283,7 +283,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender)) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 - val channelFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val channelFeeratePerKw = Globals.feeratesPerKw.get().getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ebbe469200..95b2c1ee28 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -75,7 +75,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - feeTargets = FeeConf(3, 3, 3), + feeTargets = FeeConf(6, 2, 2), maxFeerateMismatch = 1.5, updateFeeMinDiffRatio = 0.1, autoReconnect = false, @@ -141,7 +141,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - feeTargets = FeeConf(3, 3, 3), + feeTargets = FeeConf(6, 2, 2), maxFeerateMismatch = 1.0, updateFeeMinDiffRatio = 0.1, autoReconnect = false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index b37eb9418b..e40ce5d300 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -680,7 +680,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) - alice2bob.expectMsgType[UpdateFee] + val updateFee = alice2bob.expectMsgType[UpdateFee] + assert(updateFee.feeratePerKw == TestConstants.feeratePerKw + 1000) alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1403,11 +1404,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ + Globals.feeratesPerKw.set(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000)) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() - sender.send(bob, UpdateFee(ByteVector32.Zeroes, 85000)) + // Alice will use $localFeeRate when performing the checks for update_fee + val localFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget) + assert(localFeeRate === 2000) + val remoteFeeUpdate = 85000 + sender.send(bob, UpdateFee(ByteVector32.Zeroes, remoteFeeUpdate)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000") + assert(new String(error.data.toArray) === s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeUpdate localFeeratePerKw=$localFeeRate") awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -1420,6 +1426,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw == Globals.feeratesPerKw.get().getRatePerBlock(Bob.nodeParams.feeTargets.commitmentBlockTarget)) sender.send(bob, UpdateFee(ByteVector32.Zeroes, 252)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") @@ -1685,9 +1692,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index dd73f294f4..4a9567c56b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -20,8 +20,9 @@ import java.util.UUID import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.{PrivateKey} +import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} +import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ @@ -645,9 +646,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => From 0fbf708ffa8d72d864a0a172cc579cc62b59f5f5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jul 2019 13:00:10 +0200 Subject: [PATCH 03/11] Use a custom block target for funding transaction --- .../fr/acinq/eclair/channel/Channel.scala | 6 ++-- .../acinq/eclair/channel/ChannelEvents.scala | 2 +- .../fr/acinq/eclair/channel/Register.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 28 +++++++++++++++---- .../fr/acinq/eclair/gui/GUIUpdater.scala | 2 +- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 97f8bec288..87e279c156 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -145,8 +145,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, _, localParams, remote, _, channelFlags), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags), Nothing) => + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, @@ -270,7 +270,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Try(Helpers.validateParamsFundee(nodeParams, open)) match { case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId)) + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId, open.feeratePerKw, None)) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 6b3c128d22..e9104a7db4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} trait ChannelEvent -case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32) extends ChannelEvent +case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Option[Long]) extends ChannelEvent case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: ByteVector32, currentData: HasCommitments) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index 8d3794de0a..0f65bcecd6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -37,7 +37,7 @@ class Register extends Actor with ActorLogging { override def receive: Receive = main(Map.empty, Map.empty, Map.empty) def main(channels: Map[ByteVector32, ActorRef], shortIds: Map[ShortChannelId, ByteVector32], channelsTo: Map[ByteVector32, PublicKey]): Receive = { - case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) => + case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId, _, _) => context.watch(channel) context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 4264372f51..b6eeccf7dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -284,7 +284,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 val channelFeeratePerKw = Globals.feeratesPerKw.get().getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) - val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6) + val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.getRatePerBlock(nodeParams.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 51969071f3..939e50ff36 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -18,16 +18,17 @@ package fr.acinq.eclair.io import java.net.{Inet4Address, InetSocketAddress} -import akka.actor.{ActorRef, PoisonPill, Terminated} +import akka.actor.{ActorRef, ActorSystem, FSM, PoisonPill, Terminated} import java.net.{Inet4Address, InetAddress, InetSocketAddress, ServerSocket} -import akka.actor.{ActorRef, ActorSystem, PoisonPill} + import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.EclairWallet -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} +import fr.acinq.eclair.channel.{ChannelCreated, Data, HasCommitments, State} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.ChannelStateSpec import fr.acinq.eclair.io.Peer._ @@ -36,6 +37,7 @@ import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Reb import fr.acinq.eclair.wire.{Color, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector + import scala.concurrent.duration._ class PeerSpec extends TestkitBaseClass { @@ -67,7 +69,7 @@ class PeerSpec extends TestkitBaseClass { val relayer = TestProbe() val connection = TestProbe() val transport = TestProbe() - val wallet: EclairWallet = null // unused + val wallet: EclairWallet = new TestWallet val remoteNodeId = Bob.nodeParams.nodeId val peer: TestFSMRef[Peer.State, Peer.Data, Peer] = TestFSMRef(new Peer(aliceParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) @@ -239,6 +241,22 @@ class PeerSpec extends TestkitBaseClass { probe.expectMsg("disconnecting") } + test("use correct fee rates when spawning a channel") { f => + import f._ + + val probe = TestProbe() + system.eventStream.subscribe(probe.ref, classOf[ChannelCreated]) + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + + assert(peer.stateData.channels.isEmpty) + probe.send(peer, Peer.OpenChannel(remoteNodeId, Satoshi(12300), MilliSatoshi(0), None, None, None)) + awaitCond(peer.stateData.channels.nonEmpty) + + val channelCreated = probe.expectMsgType[ChannelCreated] + assert(channelCreated.initialFeeratePerKw == Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget)) + assert(channelCreated.fundingTxFeeratePerKw.get == Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.fundingBlockTarget)) + } + test("reply to ping") { f => import f._ val probe = TestProbe() diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index d63969c9b0..ed96e755f0 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -75,7 +75,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging def main(m: Map[ActorRef, ChannelPaneController]): Receive = { - case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId) => + case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId, _, _) => context.watch(channel) val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, temporaryChannelId) runInGuiThread(() => mainController.channelBox.getChildren.addAll(root)) From f0627fdc28bd06681658aeb3853ee28f2c9572aa Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jul 2019 14:20:49 +0200 Subject: [PATCH 04/11] Use custom block target for mutual close transaction --- .../fr/acinq/eclair/channel/Channel.scala | 10 ++++----- .../fr/acinq/eclair/channel/Helpers.scala | 15 ++++++++----- .../channel/states/h/ClosingStateSpec.scala | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 87e279c156..33f54dce0c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -836,7 +836,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // there are no pending signed htlcs, let's go directly to NEGOTIATING if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1079,7 +1079,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (commitments1.hasNoPendingHtlcs) { if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1115,7 +1115,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug(s"switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}") if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1169,7 +1169,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).map(Satoshi) val nextClosingFee = Closing.nextClosingFee( - localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), + localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(nodeParams, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), remoteClosingFee = Satoshi(remoteClosingFee)) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (Some(nextClosingFee) == lastLocalClosingFee) { @@ -1551,7 +1551,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them if (d.commitments.localParams.isFunder) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) goto(NEGOTIATING) using store(d.copy(closingTxProposed = closingTxProposed1)) sending d.localShutdown :: closingSigned :: Nil } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index c98b9a5965..6516ecfe85 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -423,21 +423,26 @@ object Helpers { } } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { + def firstClosingFee(feeratePerKw: Long, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { import commitments._ // this is just to estimate the weight, it depends on size of the pubkey scripts val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) - // no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw) log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx") Transactions.weight2fee(feeratePerKw, closingWeight) } + def firstClosingFee(nodeParams: NodeParams, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + val blockTarget = Math.max(nodeParams.feeTargets.mutualCloseBlockTarget, nodeParams.feeTargets.commitmentBlockTarget) + val feeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(blockTarget) + firstClosingFee(feeratePerKw, commitments, localScriptPubkey, remoteScriptPubkey) + } + def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey) + def makeFirstClosingTx(nodeParams: NodeParams, keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFee = firstClosingFee(nodeParams, commitments, localScriptPubkey, remoteScriptPubkey) makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ec19d818e7..3adbae865f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -21,10 +21,12 @@ import java.util.UUID import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} +import com.typesafe.sslconfig.util.NoopLogger import fr.acinq.bitcoin.{ByteVector32, OutPoint, ScriptFlags, Transaction, TxIn} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment._ @@ -137,6 +139,26 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // both nodes are now in CLOSING state with a mutual close tx pending for confirmation } + test("start fee negotiation from configured block target") { f => + import f._ + + Globals.feeratesPerKw.set(FeeratesPerKw(100, 250, 350, 450, 600, 800)) + + val sender = TestProbe() + // alice initiates a closing + sender.send(alice, CMD_CLOSE(None)) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + val closing = alice2bob.expectMsgType[ClosingSigned] + val aliceData = alice.stateData.asInstanceOf[DATA_NEGOTIATING] + val mutualClosingFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.mutualCloseBlockTarget) + val expectedFirstProposedFee = Closing.firstClosingFee(mutualClosingFeeRate, aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey)(akka.event.NoLogging) + assert(Alice.nodeParams.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == 250) + assert(closing.feeSatoshis == expectedFirstProposedFee.amount) + } + test("recv BITCOIN_FUNDING_PUBLISH_FAILED", Tag("funding_unconfirmed")) { f => import f._ alice ! CMD_FORCECLOSE From 4d1a02e5db29387b39343a0b2b80253a99aa5842 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jul 2019 17:42:10 +0200 Subject: [PATCH 05/11] Use custom fee target for claim main output transaction --- eclair-core/src/main/resources/reference.conf | 7 ++++--- .../scala/fr/acinq/eclair/NodeParams.scala | 9 +++++---- .../fr/acinq/eclair/channel/Channel.scala | 18 +++++++++--------- .../fr/acinq/eclair/channel/Commitments.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 19 ++++++++----------- .../eclair/transactions/Transactions.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +++--- .../channel/states/e/NormalStateSpec.scala | 11 +++++++++-- 8 files changed, 40 insertions(+), 34 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 75a2935faa..4afbf55cc8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -42,9 +42,10 @@ eclair { // number of blocks to target when computing fees for each transaction type, possible values are: 1, 2, 6, 12, 36, 72 fee-targets { - funding = 6 // confirmation block target for the funding transaction - commitment = 2 // confirmation block target for the commitment transaction (used in force-close scenario) - mutual-close = 12 // confirmation block target for the mutual close transaction + funding = 6 // target for the funding transaction + commitment = 2 // target for the commitment transaction (used in force-close scenario) + mutual-close = 12 // target for the mutual close transaction + claim-main = 6 // target for the claim main transaction (tx that spends main channel output back to wallet) } min-feerate = 3 // minimum feerate in satoshis per byte diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e667d75ad2..418fe1bdeb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.transactions.Transactions.FeeConf +import fr.acinq.eclair.transactions.Transactions.FeeTargets import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector import scala.collection.JavaConversions._ @@ -62,7 +62,7 @@ case class NodeParams(keyManager: KeyManager, pingInterval: FiniteDuration, pingTimeout: FiniteDuration, pingDisconnect: Boolean, - feeTargets: FeeConf, + feeTargets: FeeTargets, maxFeerateMismatch: Double, updateFeeMinDiffRatio: Double, autoReconnect: Boolean, @@ -175,10 +175,11 @@ object NodeParams { .toList .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt - val feeTargets = FeeConf( + val feeTargets = FeeTargets( fundingBlockTarget = config.getInt("fee-targets.funding"), commitmentBlockTarget = config.getInt("fee-targets.commitment"), - mutualCloseBlockTarget = config.getInt("fee-targets.mutual-close") + mutualCloseBlockTarget = config.getInt("fee-targets.mutual-close"), + claimMainBlockTarget = config.getInt("fee-targets.claim-main") ) NodeParams( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 33f54dce0c..c0f2f9bb97 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1214,19 +1214,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") val localCommitPublished1 = d.localCommitPublished.map { case localCommitPublished => - val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx) + val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, localCommitPublished.commitTx) doPublish(localCommitPublished1) localCommitPublished1 } val remoteCommitPublished1 = d.remoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) doPublish(remoteCommitPublished1) remoteCommitPublished1 } val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) doPublish(remoteCommitPublished1) remoteCommitPublished1 } @@ -1906,7 +1906,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else { val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, commitTx) doPublish(localCommitPublished) val nextData = d match { @@ -1981,7 +1981,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, d.commitments.remoteCommit, commitTx) doPublish(remoteCommitPublished) val nextData = d match { @@ -1998,7 +1998,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") // if we are in this state, then this field is defined val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint.get - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(nodeParams.feeTargets, keyManager, d.commitments, remotePerCommitmentPoint, commitTx) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) doPublish(remoteCommitPublished) @@ -2011,7 +2011,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val remoteCommit = d.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit require(commitTx.txid == remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, remoteCommit, commitTx) doPublish(remoteCommitPublished) val nextData = d match { @@ -2044,7 +2044,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { log.warning(s"funding tx spent in txid=${tx.txid}") - Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels) match { + Helpers.Closing.claimRevokedRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, tx, nodeParams.db.channels) match { case Some(revokedCommitPublished) => log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") val exc = FundingTxSpent(d.channelId, tx) @@ -2091,7 +2091,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's try to spend our current local tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, commitTx) doPublish(localCommitPublished) goto(ERR_INFORMATION_LEAK) sending error diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 42119432a3..6876775e89 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -310,7 +310,7 @@ object Commitments { (commitments1, fee) } - def receiveFee(feeTargets: FeeConf, commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { + def receiveFee(feeTargets: FeeTargets, commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { if (commitments.localParams.isFunder) { throw FundeeCannotSendUpdateFee(commitments.channelId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 6516ecfe85..1bf5159f70 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -495,7 +495,7 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @return a list of transactions (one per HTLC that we can claim) */ - def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { + def claimCurrentLocalCommitTxOutputs(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") @@ -503,8 +503,7 @@ object Helpers { val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) - // no need to use a high fee rate for delayed transactions (we are the only one who can spend them) - val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwDelayed = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { @@ -567,7 +566,7 @@ object Helpers { * @param tx the remote commitment transaction that has just been published * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimRemoteCommitTxOutputs(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { import commitments.{commitInput, localParams, remoteParams} require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) @@ -607,7 +606,7 @@ object Helpers { }) }.toSeq.flatten - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( + claimRemoteCommitMainOutput(feeTargets, keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } ) @@ -624,11 +623,10 @@ object Helpers { * @param tx the remote commitment transaction that has just been published * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimRemoteCommitMainOutput(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - // no need to use a high fee rate for our main output (we are the only one who can spend it) - val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwMain = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output")(Try { val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), @@ -655,7 +653,7 @@ object Helpers { * * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment */ - def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { + def claimRevokedRemoteCommitTxOutputs(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) @@ -674,8 +672,7 @@ object Helpers { val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - // no need to use a high fee rate for our main output (we are the only one who can spend it) - val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwMain = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty val feeratePerKwPenalty = Globals.feeratesPerKw.get.blocks_2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 894b45e979..b7b688681d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -65,7 +65,7 @@ object Transactions { case object OutputNotFound extends RuntimeException(s"output not found (probably trimmed)") with TxGenerationSkipped case object AmountBelowDustLimit extends RuntimeException(s"amount is below dust limit") with TxGenerationSkipped - case class FeeConf(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int) + case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) // @formatter:on /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 95b2c1ee28..777f48d693 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf -import fr.acinq.eclair.transactions.Transactions.FeeConf +import fr.acinq.eclair.transactions.Transactions.FeeTargets import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector @@ -75,7 +75,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - feeTargets = FeeConf(6, 2, 2), + feeTargets = FeeTargets(6, 2, 2, 6), maxFeerateMismatch = 1.5, updateFeeMinDiffRatio = 0.1, autoReconnect = false, @@ -141,7 +141,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - feeTargets = FeeConf(6, 2, 2), + feeTargets = FeeTargets(6, 2, 2, 6), maxFeerateMismatch = 1.0, updateFeeMinDiffRatio = 0.1, autoReconnect = false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index e40ce5d300..2b79c07fc3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{htlcSuccessWeight, htlcTimeoutWeight, weight2fee} -import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.transactions.{IN, OUT, Transactions} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} @@ -1756,6 +1756,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // in response to that, alice publishes its claim txes val claimTxes = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx + val claimMain = claimTxes(0) // in addition to its main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { assert(claimHtlcTx.txIn.size == 1) @@ -1767,7 +1768,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(amountClaimed == Satoshi(814880)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimMain)) // claim-main assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) @@ -1777,6 +1778,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) + + // assert the feerate of the claim main is what we expect + val expectedFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.claimMainBlockTarget) + val expectedFee = Transactions.weight2fee(expectedFeeRate, claimMain.weight()) + val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount.toLong).sum - claimMain.txOut.map(_.amount.toLong).sum + assert(claimFee == expectedFee.toLong) } test("recv BITCOIN_FUNDING_SPENT (their *next* commit w/ htlc)") { f => From 5c5d8570bd517ca1bb494c5f67d8e40e9c6e20e9 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jul 2019 17:51:00 +0200 Subject: [PATCH 06/11] Set fee target defaults to existing values --- eclair-core/src/main/resources/reference.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 4afbf55cc8..fb87d4b6d7 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -44,7 +44,7 @@ eclair { fee-targets { funding = 6 // target for the funding transaction commitment = 2 // target for the commitment transaction (used in force-close scenario) - mutual-close = 12 // target for the mutual close transaction + mutual-close = 6 // target for the mutual close transaction claim-main = 6 // target for the claim main transaction (tx that spends main channel output back to wallet) } From 59d96dee379c275b19b465530268c630ce4bbeaa Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Jul 2019 10:48:49 +0200 Subject: [PATCH 07/11] Use `claimP2WPKHOutputWeight` to estimate claim transaction fees in test --- .../fr/acinq/eclair/channel/states/e/NormalStateSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 2b79c07fc3..3c04095556 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1781,9 +1781,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // assert the feerate of the claim main is what we expect val expectedFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.claimMainBlockTarget) - val expectedFee = Transactions.weight2fee(expectedFeeRate, claimMain.weight()) + val expectedFee = Transactions.weight2fee(expectedFeeRate, Transactions.claimP2WPKHOutputWeight).toLong val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount.toLong).sum - claimMain.txOut.map(_.amount.toLong).sum - assert(claimFee == expectedFee.toLong) + assert(claimFee == expectedFee) } test("recv BITCOIN_FUNDING_SPENT (their *next* commit w/ htlc)") { f => From 7d0ecb6fe441762c0fb4ceb4087651986e4937bd Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Jul 2019 12:35:56 +0200 Subject: [PATCH 08/11] Add block target 144 --- eclair-core/src/main/resources/reference.conf | 3 ++- .../src/main/scala/fr/acinq/eclair/Setup.scala | 3 ++- .../blockchain/fee/BitcoinCoreFeeProvider.scala | 4 +++- .../eclair/blockchain/fee/BitgoFeeProvider.scala | 3 ++- .../blockchain/fee/EarnDotComFeeProvider.scala | 3 ++- .../fr/acinq/eclair/blockchain/fee/FeeProvider.scala | 10 ++++++---- .../eclair/blockchain/fee/SmoothFeeProvider.scala | 3 ++- .../blockchain/fee/BitcoinCoreFeeProviderSpec.scala | 10 ++++++---- .../eclair/blockchain/fee/BitgoFeeProviderSpec.scala | 3 ++- .../blockchain/fee/EarnDotComFeeProviderSpec.scala | 3 ++- .../blockchain/fee/FallbackFeeProviderSpec.scala | 6 +++--- .../blockchain/fee/SmoothFeeProviderSpec.scala | 12 ++++++------ .../eclair/channel/states/e/NormalStateSpec.scala | 4 ++-- .../eclair/channel/states/f/ShutdownStateSpec.scala | 2 +- .../eclair/channel/states/h/ClosingStateSpec.scala | 2 +- 15 files changed, 42 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index fb87d4b6d7..827b03ab28 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -37,10 +37,11 @@ eclair { 12 = 110000 36 = 50000 72 = 20000 + 144 = 15000 } } - // number of blocks to target when computing fees for each transaction type, possible values are: 1, 2, 6, 12, 36, 72 + // number of blocks to target when computing fees for each transaction type, possible values are: 1, 2, 6, 12, 36, 72, 144 fee-targets { funding = 6 // target for the funding transaction commitment = 2 // target for the commitment transaction (used in force-close scenario) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 4a28c24770..92ca2dcb61 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -188,7 +188,8 @@ class Setup(datadir: File, 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") + blocks_72 = config.getLong("default-feerates.delay-blocks.72"), + blocks_144 = config.getLong("default-feerates.delay-blocks.144") ) Globals.feeratesPerKB.set(confDefaultFeerates) Globals.feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala index 12430937f6..b25bf1d900 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -46,13 +46,15 @@ class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: F blocks_12 <- estimateSmartFee(12) blocks_36 <- estimateSmartFee(36) blocks_72 <- estimateSmartFee(72) + blocks_144 <- estimateSmartFee(144) } yield FeeratesPerKB( block_1 = if (block_1 > 0) block_1 else defaultFeerates.block_1, blocks_2 = if (blocks_2 > 0) blocks_2 else defaultFeerates.blocks_2, blocks_6 = if (blocks_6 > 0) blocks_6 else defaultFeerates.blocks_6, blocks_12 = if (blocks_12 > 0) blocks_12 else defaultFeerates.blocks_12, blocks_36 = if (blocks_36 > 0) blocks_36 else defaultFeerates.blocks_36, - blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72) + blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72, + blocks_144 = if (blocks_144 > 0) blocks_144 else defaultFeerates.blocks_144) } object BitcoinCoreFeeProvider { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala index 14c84d6d0e..97e1ed5fa3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala @@ -72,6 +72,7 @@ object BitgoFeeProvider { blocks_6 = extractFeerate(feeRanges, 6), blocks_12 = extractFeerate(feeRanges, 12), blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72)) + blocks_72 = extractFeerate(feeRanges, 72), + blocks_144 = extractFeerate(feeRanges, 144)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala index b48f85ed5a..8dcc8982bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala @@ -76,6 +76,7 @@ object EarnDotComFeeProvider { blocks_6 = extractFeerate(feeRanges, 6), blocks_12 = extractFeerate(feeRanges, 12), blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72)) + blocks_72 = extractFeerate(feeRanges, 72), + blocks_144 = extractFeerate(feeRanges, 144)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index fe71fa1a1a..b6405ac0d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -30,12 +30,12 @@ trait FeeProvider { } // stores fee rate in satoshi/kb (1 kb = 1000 bytes) -case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) { +case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long, blocks_144: 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") } // stores fee rate in satoshi/kw (1 kw = 1000 weight units) -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, blocks_144: 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") def getRatePerBlock(blockTarget: Int): Long = blockTarget match { @@ -57,7 +57,8 @@ object FeeratesPerKw { blocks_6 = feerateKB2Kw(feerates.blocks_6), blocks_12 = feerateKB2Kw(feerates.blocks_12), blocks_36 = feerateKB2Kw(feerates.blocks_36), - blocks_72 = feerateKB2Kw(feerates.blocks_72)) + blocks_72 = feerateKB2Kw(feerates.blocks_72), + blocks_144 = feerateKB2Kw(feerates.blocks_144)) /** * Used in tests @@ -71,6 +72,7 @@ object FeeratesPerKw { blocks_6 = feeratePerKw, blocks_12 = feeratePerKw, blocks_36 = feeratePerKw, - blocks_72 = feeratePerKw) + blocks_72 = feeratePerKw, + blocks_144 = feeratePerKw) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala index d805483880..35e61f31f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala @@ -47,5 +47,6 @@ object SmoothFeeProvider { 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))) + blocks_72 = avg(rates.map(_.blocks_72)), + blocks_144 = avg(rates.map(_.blocks_144))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 0a671a648d..b129f52192 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -88,7 +88,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, 7)) val sender = TestProbe() regtestProvider.getFeerates.pipeTo(sender.ref) assert(sender.expectMsgType[Failure].cause.asInstanceOf[RuntimeException].getMessage.contains("Insufficient data or no feerate found")) @@ -99,7 +99,8 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco 6 -> 1300, 12 -> 1200, 36 -> 1100, - 72 -> 1000 + 72 -> 1000, + 144 -> 900 ) val ref = FeeratesPerKB( @@ -108,7 +109,8 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco blocks_6 = fees(6), blocks_12 = fees(12), blocks_36 = fees(36), - blocks_72 = fees(72)) + blocks_72 = fees(72), + blocks_144 = fees(144)) val mockBitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), @@ -124,7 +126,7 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco } } - val mockProvider = new BitcoinCoreFeeProvider(mockBitcoinClient, FeeratesPerKB(1, 2, 3, 4, 5, 6)) + val mockProvider = new BitcoinCoreFeeProvider(mockBitcoinClient, FeeratesPerKB(1, 2, 3, 4, 5, 6, 7)) mockProvider.getFeerates.pipeTo(sender.ref) assert(sender.expectMsgType[FeeratesPerKB] == ref) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala index 263da95fd2..bfa1494e25 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala @@ -60,7 +60,8 @@ class BitgoFeeProviderSpec extends FunSuite { blocks_6 = 105566, blocks_12 = 96254, blocks_36 = 71098, - blocks_72 = 68182) + blocks_72 = 68182, + blocks_144 = 16577) assert(feerates === ref) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index 4f1cfe3b48..9b3f953b07 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -63,7 +63,8 @@ class EarnDotComFeeProviderSpec extends FunSuite with Logging { blocks_6 = 230 * 1000, blocks_12 = 140 * 1000, blocks_36 = 60 * 1000, - blocks_72 = 40 * 1000) + blocks_72 = 40 * 1000, + blocks_144 = 10 * 1000) assert(feerates === ref) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index bf70cdfd02..a742fa6eb2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -43,7 +43,7 @@ class FallbackFeeProviderSpec extends FunSuite { } else Future.failed(new RuntimeException()) } - def dummyFeerates = FeeratesPerKB(1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000)) + def dummyFeerates = FeeratesPerKB(1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000)) def await[T](f: Future[T]): T = Await.result(f, 3 seconds) @@ -73,9 +73,9 @@ class FallbackFeeProviderSpec extends FunSuite { } test("ensure minimum feerate") { - val constantFeeProvider = new ConstantFeeProvider(FeeratesPerKB(1000, 1000, 1000, 1000, 1000, 1000)) + val constantFeeProvider = new ConstantFeeProvider(FeeratesPerKB(1000, 1000, 1000, 1000, 1000, 1000, 1000)) val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, 2) - assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(2000, 2000, 2000, 2000, 2000, 2000)) + assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(2000, 2000, 2000, 2000, 2000, 2000, 1000)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 6c0e514ef4..a38546dc3b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -26,11 +26,11 @@ import scala.concurrent.{Await, Future} 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) + FeeratesPerKB(100, 200, 300, 400, 500, 600, 650), + FeeratesPerKB(200, 300, 400, 500, 600, 700, 750), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850) ) val provider = new FeeProvider { var index = 0 @@ -55,7 +55,7 @@ class SmoothFeeProviderSpec extends FunSuite { 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(rate3 == FeeratesPerKB(200, 300, 400, 500, 600, 700, 750)) assert(rate4 == SmoothFeeProvider.smooth(Seq(rates(1), rates(2), rates(3)))) assert(rate5 == rates(4)) // since the last 3 values are the same } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 3c04095556..023a3ab348 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1404,7 +1404,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ - Globals.feeratesPerKw.set(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000)) + Globals.feeratesPerKw.set(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000, 140000)) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() // Alice will use $localFeeRate when performing the checks for update_fee @@ -1692,7 +1692,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200, 14400)) sender.send(alice, event) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4a9567c56b..dadfad2623 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -646,7 +646,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200, 14400)) sender.send(alice, event) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 3adbae865f..6e035cb77e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -142,7 +142,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("start fee negotiation from configured block target") { f => import f._ - Globals.feeratesPerKw.set(FeeratesPerKw(100, 250, 350, 450, 600, 800)) + Globals.feeratesPerKw.set(FeeratesPerKw(100, 250, 350, 450, 600, 800, 900)) val sender = TestProbe() // alice initiates a closing From 8099297111355de9bae39ee0eb739395618ae9f0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Jul 2019 14:24:35 +0200 Subject: [PATCH 09/11] Fix bitgo fee provider spec --- .../eclair/blockchain/fee/BitgoFeeProviderSpec.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala index bfa1494e25..23ed6d88f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala @@ -18,9 +18,13 @@ package fr.acinq.eclair.blockchain.fee import akka.actor.ActorSystem import akka.util.Timeout +import com.softwaremill.sttp.okhttp.{OkHttpBackend, OkHttpFutureBackend} +import fr.acinq.bitcoin.Block import org.json4s.DefaultFormats import org.scalatest.FunSuite +import scala.concurrent.Await + /** * Created by PM on 27/01/2017. */ @@ -68,7 +72,11 @@ class BitgoFeeProviderSpec extends FunSuite { test("make sure API hasn't changed") { import scala.concurrent.duration._ implicit val system = ActorSystem() + implicit val ec = system.dispatcher + implicit val sttp = OkHttpFutureBackend() implicit val timeout = Timeout(30 seconds) + val bitgo = new BitgoFeeProvider(Block.LivenetGenesisBlock.hash) + assert(Await.result(bitgo.getFeerates, timeout.duration).block_1 > 0) } } From 5195614413a06fd1616a3d87b299fce1c89bb954 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Jul 2019 16:21:30 +0200 Subject: [PATCH 10/11] Allow choosing blockTarget = 144 --- .../main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index b6405ac0d4..dee86da916 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -45,6 +45,7 @@ case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_1 case 12 => blocks_12 case 36 => blocks_36 case 72 => blocks_72 + case 144 => blocks_144 case _ => throw new IllegalArgumentException(s"can't choose ratePerBlock=$blockTarget") } From 63becf307565c92ca3d4c461a5bb163a0b6a58f0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Jul 2019 16:35:23 +0200 Subject: [PATCH 11/11] Move 'feerates' parameter to last position, pass `FeeTargets` instead of `NodeParams` --- .../fr/acinq/eclair/channel/Channel.scala | 24 +++++++++---------- .../fr/acinq/eclair/channel/Helpers.scala | 17 ++++++------- .../channel/states/h/ClosingStateSpec.scala | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index c0f2f9bb97..74ce207a6f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -836,7 +836,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // there are no pending signed htlcs, let's go directly to NEGOTIATING if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1079,7 +1079,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (commitments1.hasNoPendingHtlcs) { if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1115,7 +1115,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug(s"switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}") if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1169,7 +1169,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).map(Satoshi) val nextClosingFee = Closing.nextClosingFee( - localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(nodeParams, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), + localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.feeTargets)), remoteClosingFee = Satoshi(remoteClosingFee)) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (Some(nextClosingFee) == lastLocalClosingFee) { @@ -1214,19 +1214,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") val localCommitPublished1 = d.localCommitPublished.map { case localCommitPublished => - val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, localCommitPublished.commitTx) + val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.feeTargets) doPublish(localCommitPublished1) localCommitPublished1 } val remoteCommitPublished1 = d.remoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } @@ -1551,7 +1551,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them if (d.commitments.localParams.isFunder) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(nodeParams, keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.feeTargets) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) goto(NEGOTIATING) using store(d.copy(closingTxProposed = closingTxProposed1)) sending d.localShutdown :: closingSigned :: Nil } else { @@ -1906,7 +1906,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else { val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.feeTargets) doPublish(localCommitPublished) val nextData = d match { @@ -1981,7 +1981,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, d.commitments.remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -2011,7 +2011,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val remoteCommit = d.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit require(commitTx.txid == remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -2091,7 +2091,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's try to spend our current local tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.feeTargets) doPublish(localCommitPublished) goto(ERR_INFORMATION_LEAK) sending error diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 1bf5159f70..caff890a55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -423,7 +423,7 @@ object Helpers { } } - def firstClosingFee(feeratePerKw: Long, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeratePerKw: Long)(implicit log: LoggingAdapter): Satoshi = { import commitments._ // this is just to estimate the weight, it depends on size of the pubkey scripts val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) @@ -432,17 +432,17 @@ object Helpers { Transactions.weight2fee(feeratePerKw, closingWeight) } - def firstClosingFee(nodeParams: NodeParams, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Satoshi = { // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - val blockTarget = Math.max(nodeParams.feeTargets.mutualCloseBlockTarget, nodeParams.feeTargets.commitmentBlockTarget) + val blockTarget = Math.max(feeTargets.mutualCloseBlockTarget, feeTargets.commitmentBlockTarget) val feeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(blockTarget) - firstClosingFee(feeratePerKw, commitments, localScriptPubkey, remoteScriptPubkey) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeratePerKw) } def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(nodeParams: NodeParams, keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - val closingFee = firstClosingFee(nodeParams, commitments, localScriptPubkey, remoteScriptPubkey) + def makeFirstClosingTx(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeTargets) makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee) } @@ -495,7 +495,7 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @return a list of transactions (one per HTLC that we can claim) */ - def claimCurrentLocalCommitTxOutputs(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { + def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") @@ -564,9 +564,10 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) * @param tx the remote commitment transaction that has just been published + * @param feeTargets the block target to use when computing fees for the transactions * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitTxOutputs(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { import commitments.{commitInput, localParams, remoteParams} require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 6e035cb77e..41ed99ff97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -154,7 +154,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val closing = alice2bob.expectMsgType[ClosingSigned] val aliceData = alice.stateData.asInstanceOf[DATA_NEGOTIATING] val mutualClosingFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.mutualCloseBlockTarget) - val expectedFirstProposedFee = Closing.firstClosingFee(mutualClosingFeeRate, aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey)(akka.event.NoLogging) + val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) assert(Alice.nodeParams.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == 250) assert(closing.feeSatoshis == expectedFirstProposedFee.amount) }