diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index fc78e6c470..cadbbec31b 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -29,16 +29,6 @@ eclair { zmqtx = "tcp://127.0.0.1:29000" } - default-feerates { // those are in satoshis per kilobyte - delay-blocks { - 1 = 210000 - 2 = 180000 - 6 = 150000 - 12 = 110000 - 36 = 50000 - 72 = 20000 - } - } min-feerate = 3 // minimum feerate in satoshis per byte smooth-feerate-window = 6 // 1 = no smoothing @@ -77,13 +67,33 @@ eclair { fee-base-msat = 1000 fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.01%) - // maximum local vs remote feerate mismatch; 1.0 means 100% - // actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch - max-feerate-mismatch = 1.56 // will allow remote fee rates up to 8x bigger or smaller than our local fee rate + on-chain-fees { + default-feerates { // those are per target block, in satoshis per kilobyte + 1 = 210000 + 2 = 180000 + 6 = 150000 + 12 = 110000 + 36 = 50000 + 72 = 20000 + 144 = 15000 + } - // funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater - // than this ratio. - update-fee_min-diff-ratio = 0.1 + // number of blocks to target when computing fees for each transaction type + target-blocks { + funding = 6 // target for the funding transaction + commitment = 2 // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing* + mutual-close = 12 // target for the mutual close transaction + claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet) + } + + // maximum local vs remote feerate mismatch; 1.0 means 100% + // actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch + max-feerate-mismatch = 1.56 // will allow remote fee rates up to 8x bigger or smaller than our local fee rate + + // funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater + // than this ratio. + update-fee-min-diff-ratio = 0.1 + } revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting 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 d707fa9e6c..55f5fb2859 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -24,7 +24,8 @@ import java.util.concurrent.TimeUnit import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32} -import fr.acinq.eclair.NodeParams.WatcherType +import fr.acinq.eclair.NodeParams.{WatcherType} +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, OnChainFeeConf} import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ @@ -47,6 +48,7 @@ case class NodeParams(keyManager: KeyManager, localFeatures: ByteVector, overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)], dustLimitSatoshis: Long, + onChainFeeConf: OnChainFeeConf, maxHtlcValueInFlightMsat: UInt64, maxAcceptedHtlcs: Int, expiryDeltaBlocks: Int, @@ -55,7 +57,6 @@ case class NodeParams(keyManager: KeyManager, toRemoteDelayBlocks: Int, maxToLocalDelayBlocks: Int, minDepthBlocks: Int, - smartfeeNBlocks: Int, feeBaseMsat: Int, feeProportionalMillionth: Int, reserveToFundingRatio: Double, @@ -65,8 +66,6 @@ case class NodeParams(keyManager: KeyManager, pingInterval: FiniteDuration, pingTimeout: FiniteDuration, pingDisconnect: Boolean, - maxFeerateMismatch: Double, - updateFeeMinDiffRatio: Double, autoReconnect: Boolean, initialRandomReconnectDelay: FiniteDuration, maxReconnectInterval: FiniteDuration, @@ -125,7 +124,7 @@ object NodeParams { } } - def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases): NodeParams = { + def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases, feeEstimator: FeeEstimator): NodeParams = { val chain = config.getString("chain") val chainHash = makeChainHash(chain) @@ -181,6 +180,13 @@ object NodeParams { .toList .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt + val feeTargets = FeeTargets( + fundingBlockTarget = config.getInt("on-chain-fees.target-blocks.funding"), + commitmentBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment"), + mutualCloseBlockTarget = config.getInt("on-chain-fees.target-blocks.mutual-close"), + claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main") + ) + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -190,6 +196,12 @@ object NodeParams { localFeatures = ByteVector.fromValidHex(config.getString("local-features")), overrideFeatures = overrideFeatures, dustLimitSatoshis = dustLimitSatoshis, + onChainFeeConf = OnChainFeeConf( + feeTargets = feeTargets, + feeEstimator = feeEstimator, + maxFeerateMismatch = config.getDouble("on-chain-fees.max-feerate-mismatch"), + updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio") + ), maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")), maxAcceptedHtlcs = maxAcceptedHtlcs, expiryDeltaBlocks = expiryDeltaBlocks, @@ -198,7 +210,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"), @@ -208,8 +219,6 @@ 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"), - maxFeerateMismatch = config.getDouble("max-feerate-mismatch"), - updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"), autoReconnect = config.getBoolean("auto-reconnect"), initialRandomReconnectDelay = FiniteDuration(config.getDuration("initial-random-reconnect-delay").getSeconds, TimeUnit.SECONDS), maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), 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..bc8ee08f41 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -96,7 +96,12 @@ class Setup(datadir: File, case None => Databases.sqliteJDBC(chaindir) } - val nodeParams = NodeParams.makeNodeParams(config, keyManager, initTor(), database) + val feeEstimator = new FeeEstimator { + override def getFeeratePerKb(target: Int): Long = Globals.feeratesPerKB.get().feePerBlock(target) + override def getFeeratePerKw(target: Int): Long = Globals.feeratesPerKw.get().feePerBlock(target) + } + + val nodeParams = NodeParams.makeNodeParams(config, keyManager, initTor(), database, feeEstimator) val serverBindingAddress = new InetSocketAddress( config.getString("server.binding-ip"), @@ -183,12 +188,13 @@ class Setup(datadir: File, defaultFeerates = { val confDefaultFeerates = FeeratesPerKB( - block_1 = config.getLong("default-feerates.delay-blocks.1"), - blocks_2 = config.getLong("default-feerates.delay-blocks.2"), - blocks_6 = config.getLong("default-feerates.delay-blocks.6"), - blocks_12 = config.getLong("default-feerates.delay-blocks.12"), - blocks_36 = config.getLong("default-feerates.delay-blocks.36"), - blocks_72 = config.getLong("default-feerates.delay-blocks.72") + block_1 = config.getLong("on-chain-fees.default-feerates.1"), + blocks_2 = config.getLong("on-chain-fees.default-feerates.2"), + blocks_6 = config.getLong("on-chain-fees.default-feerates.6"), + blocks_12 = config.getLong("on-chain-fees.default-feerates.12"), + blocks_36 = config.getLong("on-chain-fees.default-feerates.36"), + blocks_72 = config.getLong("on-chain-fees.default-feerates.72"), + blocks_144 = config.getLong("on-chain-fees.default-feerates.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/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala new file mode 100644 index 0000000000..df18977172 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.blockchain.fee + +trait FeeEstimator { + + def getFeeratePerKb(target: Int) : Long + + def getFeeratePerKw(target: Int) : Long + +} + +case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) + +case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, maxFeerateMismatch: Double, updateFeeMinDiffRatio: Double) \ No newline at end of file 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..35e7b75169 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,13 +30,33 @@ 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) { - require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0") +case class 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 && blocks_144 > 0, "all feerates must be strictly greater than 0") + + def feePerBlock(target: Int) = target match { + case 1 => block_1 + case 2 => blocks_2 + case t if t <= 6 => blocks_6 + case t if t <= 12 => blocks_12 + case t if t <= 36 => blocks_36 + case t if t <= 72 => blocks_72 + case _ => blocks_144 + } } // 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") +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 && blocks_144 > 0, "all feerates must be strictly greater than 0") + + def feePerBlock(target: Int) = target match { + case 1 => block_1 + case 2 => blocks_2 + case t if t <= 6 => blocks_6 + case t if t <= 12 => blocks_12 + case t if t <= 36 => blocks_36 + case t if t <= 72 => blocks_72 + case _ => blocks_144 + } } object FeeratesPerKw { @@ -46,7 +66,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 @@ -60,6 +81,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/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 74e2ecec7f..72585ee2fd 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, isFunder = 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, isFunder = 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, isFunder = false, open.temporaryChannelId)) + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = 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, @@ -362,7 +362,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) // signature of their initial commitment tx that pays remote pushMsat @@ -403,7 +403,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) @@ -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(d.commitments, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, fee, nodeParams.onChainFeeConf.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -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(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -852,12 +852,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentBlockCount, d: DATA_NORMAL) => handleNewBlock(c, d) case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) => - val networkFeeratePerKw = feeratesPerKw.blocks_2 + val networkFeeratePerKw = feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { - case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => + case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) stay - case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.maxFeerateMismatch) => + case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) case _ => stay } @@ -1039,7 +1039,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(d.commitments, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, fee, nodeParams.onChainFeeConf.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -1078,7 +1078,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(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1114,7 +1114,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(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1133,13 +1133,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentBlockCount, d: DATA_SHUTDOWN) => handleNewBlock(c, d) - case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => - val networkFeeratePerKw = feerates.blocks_2 + case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_SHUTDOWN) => + val networkFeeratePerKw = feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { - case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => + case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) stay - case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.maxFeerateMismatch) => + case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) case _ => stay } @@ -1167,7 +1167,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(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)), remoteClosingFee = Satoshi(remoteClosingFee)) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (lastLocalClosingFee.contains(nextClosingFee)) { @@ -1212,19 +1212,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 { localCommitPublished => - val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx) + val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(localCommitPublished1) localCommitPublished1 } val remoteCommitPublished1 = d.remoteCommitPublished.map { remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } @@ -1289,7 +1289,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => - val (rev1, tx_opt) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx) + val (rev1, tx_opt) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) tx_opt.foreach(claimTx => blockchain ! PublishAsap(claimTx)) tx_opt.foreach(claimTx => blockchain ! WatchSpent(self, tx, claimTx.txIn.head.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT)) rev1 @@ -1548,7 +1548,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(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil } else { @@ -1928,7 +1928,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(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(localCommitPublished) val nextData = d match { @@ -1993,7 +1993,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(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -2010,7 +2010,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(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) doPublish(remoteCommitPublished) @@ -2023,7 +2023,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(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -2056,7 +2056,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(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { case Some(revokedCommitPublished) => log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") val exc = FundingTxSpent(d.channelId, tx) @@ -2103,7 +2103,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(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) doPublish(localCommitPublished) goto(ERR_INFORMATION_LEAK) sending error 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/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 5e10917ee3..7c1bace272 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions._ @@ -321,7 +322,7 @@ object Commitments { (commitments1, fee) } - def receiveFee(commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { + def receiveFee(commitments: Commitments, feeEstimator: FeeEstimator, feeTargets: FeeTargets, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { if (commitments.localParams.isFunder) { throw FundeeCannotSendUpdateFee(commitments.channelId) } @@ -330,7 +331,7 @@ object Commitments { throw FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val localFeeratePerKw = feeEstimator.getFeeratePerKw(target = 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..5cbe075fcb 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 @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.eclair.blockchain.EclairWallet +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.db.ChannelsDb @@ -111,8 +112,8 @@ object Helpers { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 - if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) + val localFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) + if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) // only enforce dust limit check on mainnet if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { if (open.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT) @@ -423,21 +424,26 @@ object Helpers { } } - def firstClosingFee(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) 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(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Satoshi = { + val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + val feeratePerKw = Math.min(requestedFeerate, commitments.localCommit.spec.feeratePerKw) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeratePerKw) + } + 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(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee) } @@ -490,7 +496,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(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, 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") @@ -498,8 +504,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 = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { @@ -562,7 +567,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(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeEstimator: FeeEstimator, 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) @@ -574,7 +579,7 @@ object Helpers { val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) // we need to use a rather high fee for htlc-claim because we compete with the counterparty - val feeratePerKwHtlc = Globals.feeratesPerKw.get.blocks_2 + val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) // those are the preimages to existing received htlcs val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage } @@ -602,7 +607,7 @@ object Helpers { }) }.toSeq.flatten - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( + claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } ) @@ -619,11 +624,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(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(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 = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output")(Try { val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), @@ -650,7 +654,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(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(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) @@ -669,10 +673,9 @@ 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 = feeEstimator.getFeeratePerKw(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 + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { @@ -737,7 +740,7 @@ object Helpers { * @param htlcTx * @return */ - def claimRevokedHtlcTxOutputs(keyManager: KeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction)(implicit log: LoggingAdapter): (RevokedCommitPublished, Option[Transaction]) = { + def claimRevokedHtlcTxOutputs(keyManager: KeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Option[Transaction]) = { if (htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && !(revokedCommitPublished.claimMainOutputTx ++ revokedCommitPublished.mainPenaltyTx ++ revokedCommitPublished.htlcPenaltyTxs).map(_.txid).toSet.contains(htlcTx.txid)) { log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}") @@ -756,7 +759,7 @@ object Helpers { val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) // 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.block_1 + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) generateTx("claim-htlc-delayed-penalty")(Try { val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) 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 08969aed8d..dbc91f208e 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,8 +283,8 @@ 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 fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6) + val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) + val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.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/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 1a00b51d64..da1edf35f7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -43,7 +43,7 @@ class StartupSpec extends FunSuite { val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) // try to create a NodeParams instance with a conf that contains an illegal alias - val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb())) + val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb(), new TestConstants.TestFeeEstimator)) assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) } 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 e604a51dc2..3f5e0b3744 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -21,6 +21,7 @@ import java.sql.{Connection, DriverManager} import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Script} import fr.acinq.eclair.NodeParams.BITCOIND +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer @@ -40,6 +41,18 @@ object TestConstants { val feeratePerKw = 10000L val emptyOnionPacket = wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) + class TestFeeEstimator extends FeeEstimator { + private var currentFeerates = FeeratesPerKw.single(feeratePerKw) + + override def getFeeratePerKb(target: Int): Long = feerateKw2KB(currentFeerates.feePerBlock(target)) + + override def getFeeratePerKw(target: Int): Long = currentFeerates.feePerBlock(target) + + def setFeerate(feeratesPerKw: FeeratesPerKw): Unit = { + currentFeerates = feeratesPerKw + } + } + def sqliteInMemory() = DriverManager.getConnection("jdbc:sqlite::memory:") def inMemoryDb(connection: Connection = sqliteInMemory()): Databases = Databases.databaseByConnections(connection, connection, connection) @@ -58,6 +71,12 @@ object TestConstants { localFeatures = ByteVector(0), overrideFeatures = Map.empty, dustLimitSatoshis = 1100, + onChainFeeConf = OnChainFeeConf( + feeTargets = FeeTargets(6, 2, 2, 6), + feeEstimator = new TestFeeEstimator, + maxFeerateMismatch = 1.5, + updateFeeMinDiffRatio = 0.1 + ), maxHtlcValueInFlightMsat = UInt64(150000000), maxAcceptedHtlcs = 100, expiryDeltaBlocks = 144, @@ -66,7 +85,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -76,8 +94,6 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - maxFeerateMismatch = 1.5, - updateFeeMinDiffRatio = 0.1, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, @@ -125,6 +141,12 @@ object TestConstants { localFeatures = ByteVector.empty, // no announcement overrideFeatures = Map.empty, dustLimitSatoshis = 1000, + onChainFeeConf = OnChainFeeConf( + feeTargets = FeeTargets(6, 2, 2, 6), + feeEstimator = new TestFeeEstimator, + maxFeerateMismatch = 1.0, + updateFeeMinDiffRatio = 0.1 + ), maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs maxAcceptedHtlcs = 30, expiryDeltaBlocks = 144, @@ -133,7 +155,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -143,8 +164,6 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, - maxFeerateMismatch = 1.0, - updateFeeMinDiffRatio = 0.1, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala index 6afa9e45f2..0aaa6fd811 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala @@ -32,7 +32,6 @@ abstract class TestkitBaseClass extends TestKit(ActorSystem("test")) with fixtur override def beforeAll { Globals.blockCount.set(400000) - Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw)) } override def afterEach() { @@ -45,7 +44,6 @@ abstract class TestkitBaseClass extends TestKit(ActorSystem("test")) with fixtur override def afterAll { TestKit.shutdownActorSystem(system) - Globals.feeratesPerKw.set(FeeratesPerKw.single(1)) } } \ No newline at end of file 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..6f789a34a7 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. */ @@ -60,14 +64,19 @@ 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) } 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) } -} +} \ No newline at end of file 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/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index f2cf9e0940..ac21eeba31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -20,10 +20,11 @@ import java.util.UUID import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.blockchain.fee.FeeTargets import fr.acinq.eclair.channel._ +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ @@ -46,7 +47,6 @@ trait StateTestsHelperMethods extends TestKitBase { channelUpdateListener: TestProbe) def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: EclairWallet = new TestWallet): SetupFixture = { - Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw)) val alice2bob = TestProbe() val bob2alice = TestProbe() val alice2blockchain = TestProbe() @@ -70,8 +70,6 @@ trait StateTestsHelperMethods extends TestKitBase { val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) - // reset global feerates (they may have been changed by previous tests) - Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw)) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] @@ -165,4 +163,14 @@ trait StateTestsHelperMethods extends TestKitBase { def channelId(a: TestFSMRef[State, Data, Channel]) = Helpers.getChannelId(a.stateData) + implicit class ChannelWithTestFeeConf(a: TestFSMRef[State, Data, Channel]) { + def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets + } + + + implicit class PeerWithTestFeeConf(a: TestFSMRef[Peer.State, Peer.Data, Peer]) { + def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets + } } 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 89d0c58b8d..e327d9cad6 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 @@ -23,7 +23,7 @@ import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -35,7 +35,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, 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} @@ -683,7 +683,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) @@ -1391,8 +1392,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() val fee = UpdateFee(ByteVector32.Zeroes, 100000000) - // we first update the global variable so that we don't trigger a 'fee too different' error - Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) + // we first update the feerates so that we don't trigger a 'fee too different' error + bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) @@ -1406,11 +1407,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ + bob.feeEstimator.setFeerate(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000, 140000)) 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 = bob.feeEstimator.getFeeratePerKw(bob.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) @@ -1421,8 +1427,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFee (remote feerate is too small)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + val tx = bobCommitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() + val expectedFeeratePerKw = bob.feeEstimator.getFeeratePerKw(bob.feeTargets.commitmentBlockTarget) + assert(bobCommitments.localCommit.spec.feeratePerKw == expectedFeeratePerKw) 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") @@ -1798,9 +1807,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, 14400)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.feePerBlock(Alice.nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget))) } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => @@ -1862,6 +1871,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) @@ -1873,7 +1883,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) @@ -1883,6 +1893,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 = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.claimMainBlockTarget) + 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) } test("recv BITCOIN_FUNDING_SPENT (their *next* commit w/ htlc)") { 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 8946196271..760efbe3f6 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 @@ -22,6 +22,7 @@ import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} +import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ @@ -30,7 +31,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} -import org.scalatest.Outcome +import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -580,8 +581,8 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() val fee = UpdateFee(ByteVector32.Zeroes, 100000000) - // we first update the global variable so that we don't trigger a 'fee too different' error - Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) + // we first update the feerates so that we don't trigger a 'fee too different' error + bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 72120000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) @@ -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, 14400)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.feePerBlock(alice.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/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 85a1b45fcc..3b798fe887 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -22,9 +22,9 @@ import akka.actor.Status.Failure import akka.event.LoggingAdapter import akka.testkit.TestProbe import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} -import fr.acinq.eclair.TestConstants.Bob +import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods @@ -52,7 +52,14 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods reachNormal(setup) val sender = TestProbe() // alice initiates a closing - if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4319)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(10000)) + if (test.tags.contains("fee2")) { + alice.feeEstimator.setFeerate(FeeratesPerKw.single(4319)) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(4319)) + } + else { + alice.feeEstimator.setFeerate(FeeratesPerKw.single(10000)) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(10000)) + } sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) @@ -61,7 +68,13 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods // NB: at this point, alice has already computed and sent the first ClosingSigned message // In order to force a fee negotiation, we will change the current fee before forwarding // the Shutdown message to alice, so that alice computes a different initial closing fee. - if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4316)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(5000)) + if (test.tags.contains("fee2")) { + alice.feeEstimator.setFeerate(FeeratesPerKw.single(4316)) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(4316)) + } else { + alice.feeEstimator.setFeerate(FeeratesPerKw.single(5000)) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(5000)) + } alice2bob.forward(bob) awaitCond(bob.stateName == NEGOTIATING) withFixture(test.toNoArgTest(setup)) 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 e9054c14a5..b4715a7110 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,13 @@ 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.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKw} import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment._ @@ -137,6 +140,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._ + + alice.feeEstimator.setFeerate(FeeratesPerKw(100, 250, 350, 450, 600, 800, 900)) + + 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 = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.mutualCloseBlockTarget) + val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) + assert(alice.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 @@ -308,7 +331,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) // agreeing on a closing fee val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - Globals.feeratesPerKw.set(FeeratesPerKw.single(100)) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(100)) alice2bob.forward(bob) val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis bob2alice.forward(alice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 2e15d4ce78..6697116750 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -22,7 +22,7 @@ import java.util.concurrent.{CountDownLatch, TimeUnit} import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ @@ -53,13 +53,14 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix val relayer = paymentHandler val router = TestProbe() val wallet = new TestWallet - val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer)) - val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer)) + val feeEstimator = new TestFeeEstimator + val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer)) + val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams.copy(onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer)) val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) // alice and bob will both have 1 000 000 sat - Globals.feeratesPerKw.set(FeeratesPerKw.single(10000)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, Globals.feeratesPerKw.get.blocks_2, Globals.feeratesPerKw.get.blocks_6, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) + feeEstimator.setFeerate(FeeratesPerKw.single(10000)) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) within(30 seconds) { @@ -84,7 +85,6 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix } override def afterAll { - Globals.feeratesPerKw.set(FeeratesPerKw.single(1)) TestKit.shutdownActorSystem(system) } 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 82617ea236..76fd945de6 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 @@ -22,10 +22,12 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.{ActorRef, PoisonPill} 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.states.StateTestsHelperMethods +import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo @@ -36,7 +38,7 @@ import scodec.bits.ByteVector import scala.concurrent.duration._ -class PeerSpec extends TestkitBaseClass { +class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { def ipv4FromInet4(address: InetSocketAddress) = IPv4.apply(address.getAddress.asInstanceOf[Inet4Address], address.getPort) @@ -65,7 +67,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))) @@ -237,6 +239,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 == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.commitmentBlockTarget)) + assert(channelCreated.fundingTxFeeratePerKw.get == peer.feeEstimator.getFeeratePerKw(peer.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))