diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index b2de25ee7c..827b03ab28 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -37,8 +37,18 @@ eclair { 12 = 110000 36 = 50000 72 = 20000 + 144 = 15000 } } + + // number of blocks to target when computing fees for each transaction type, possible values are: 1, 2, 6, 12, 36, 72, 144 + fee-targets { + funding = 6 // target for the funding transaction + commitment = 2 // target for the commitment transaction (used in force-close scenario) + mutual-close = 6 // target for the mutual close transaction + claim-main = 6 // target for the claim main transaction (tx that spends main channel output back to wallet) + } + min-feerate = 3 // minimum feerate in satoshis per byte smooth-feerate-window = 6 // 1 = no smoothing diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 9266d701e7..418fe1bdeb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -20,7 +20,6 @@ import java.io.File import java.net.InetSocketAddress import java.nio.file.Files import java.util.concurrent.TimeUnit - import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32} @@ -30,9 +29,9 @@ import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams +import fr.acinq.eclair.transactions.Transactions.FeeTargets import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector - import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -54,7 +53,6 @@ case class NodeParams(keyManager: KeyManager, toRemoteDelayBlocks: Int, maxToLocalDelayBlocks: Int, minDepthBlocks: Int, - smartfeeNBlocks: Int, feeBaseMsat: Int, feeProportionalMillionth: Int, reserveToFundingRatio: Double, @@ -64,6 +62,7 @@ case class NodeParams(keyManager: KeyManager, pingInterval: FiniteDuration, pingTimeout: FiniteDuration, pingDisconnect: Boolean, + feeTargets: FeeTargets, maxFeerateMismatch: Double, updateFeeMinDiffRatio: Double, autoReconnect: Boolean, @@ -176,6 +175,13 @@ object NodeParams { .toList .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt + val feeTargets = FeeTargets( + fundingBlockTarget = config.getInt("fee-targets.funding"), + commitmentBlockTarget = config.getInt("fee-targets.commitment"), + mutualCloseBlockTarget = config.getInt("fee-targets.mutual-close"), + claimMainBlockTarget = config.getInt("fee-targets.claim-main") + ) + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -192,7 +198,6 @@ object NodeParams { toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), minDepthBlocks = config.getInt("mindepth-blocks"), - smartfeeNBlocks = 3, feeBaseMsat = config.getInt("fee-base-msat"), feeProportionalMillionth = config.getInt("fee-proportional-millionths"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), @@ -202,6 +207,7 @@ object NodeParams { pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS), pingTimeout = FiniteDuration(config.getDuration("ping-timeout").getSeconds, TimeUnit.SECONDS), pingDisconnect = config.getBoolean("ping-disconnect"), + feeTargets = feeTargets, maxFeerateMismatch = config.getDouble("max-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"), autoReconnect = config.getBoolean("auto-reconnect"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 4a28c24770..92ca2dcb61 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -188,7 +188,8 @@ class Setup(datadir: File, blocks_6 = config.getLong("default-feerates.delay-blocks.6"), blocks_12 = config.getLong("default-feerates.delay-blocks.12"), blocks_36 = config.getLong("default-feerates.delay-blocks.36"), - blocks_72 = config.getLong("default-feerates.delay-blocks.72") + blocks_72 = config.getLong("default-feerates.delay-blocks.72"), + blocks_144 = config.getLong("default-feerates.delay-blocks.144") ) Globals.feeratesPerKB.set(confDefaultFeerates) Globals.feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala index 12430937f6..b25bf1d900 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -46,13 +46,15 @@ class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: F blocks_12 <- estimateSmartFee(12) blocks_36 <- estimateSmartFee(36) blocks_72 <- estimateSmartFee(72) + blocks_144 <- estimateSmartFee(144) } yield FeeratesPerKB( block_1 = if (block_1 > 0) block_1 else defaultFeerates.block_1, blocks_2 = if (blocks_2 > 0) blocks_2 else defaultFeerates.blocks_2, blocks_6 = if (blocks_6 > 0) blocks_6 else defaultFeerates.blocks_6, blocks_12 = if (blocks_12 > 0) blocks_12 else defaultFeerates.blocks_12, blocks_36 = if (blocks_36 > 0) blocks_36 else defaultFeerates.blocks_36, - blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72) + blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72, + blocks_144 = if (blocks_144 > 0) blocks_144 else defaultFeerates.blocks_144) } object BitcoinCoreFeeProvider { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala index 14c84d6d0e..97e1ed5fa3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala @@ -72,6 +72,7 @@ object BitgoFeeProvider { blocks_6 = extractFeerate(feeRanges, 6), blocks_12 = extractFeerate(feeRanges, 12), blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72)) + blocks_72 = extractFeerate(feeRanges, 72), + blocks_144 = extractFeerate(feeRanges, 144)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala index b48f85ed5a..8dcc8982bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala @@ -76,6 +76,7 @@ object EarnDotComFeeProvider { blocks_6 = extractFeerate(feeRanges, 6), blocks_12 = extractFeerate(feeRanges, 12), blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72)) + blocks_72 = extractFeerate(feeRanges, 72), + blocks_144 = extractFeerate(feeRanges, 144)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 050b7f0a26..dee86da916 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -30,13 +30,25 @@ trait FeeProvider { } // stores fee rate in satoshi/kb (1 kb = 1000 bytes) -case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) { +case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long, blocks_144: Long) { require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0") } // stores fee rate in satoshi/kw (1 kw = 1000 weight units) -case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) { +case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long, blocks_144: Long) { require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0") + + def getRatePerBlock(blockTarget: Int): Long = blockTarget match { + case 1 => block_1 + case 2 => blocks_2 + case 6 => blocks_6 + case 12 => blocks_12 + case 36 => blocks_36 + case 72 => blocks_72 + case 144 => blocks_144 + case _ => throw new IllegalArgumentException(s"can't choose ratePerBlock=$blockTarget") + } + } object FeeratesPerKw { @@ -46,7 +58,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 +73,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 8f71a13f53..74ce207a6f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -145,8 +145,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, _, localParams, remote, _, channelFlags), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags), Nothing) => + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, @@ -270,7 +270,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Try(Helpers.validateParamsFundee(nodeParams, open)) match { case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId)) + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId, open.feeratePerKw, None)) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -681,7 +681,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case Event(fee: UpdateFee, d: DATA_NORMAL) => - Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match { + Try(Commitments.receiveFee(nodeParams.feeTargets, d.commitments, fee, nodeParams.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -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.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -853,7 +853,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) => - val networkFeeratePerKw = feeratesPerKw.blocks_2 + val networkFeeratePerKw = feeratesPerKw.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) @@ -1040,7 +1040,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case Event(fee: UpdateFee, d: DATA_SHUTDOWN) => - Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match { + Try(Commitments.receiveFee(nodeParams.feeTargets, d.commitments, fee, nodeParams.maxFeerateMismatch)) match { case Success(commitments1) => stay using d.copy(commitments = commitments1) case Failure(cause) => handleLocalError(cause, d, Some(fee)) } @@ -1079,7 +1079,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (commitments1.hasNoPendingHtlcs) { if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1115,7 +1115,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug(s"switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}") if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.feeTargets) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1135,8 +1135,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) - case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => - val networkFeeratePerKw = feerates.blocks_2 + case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_SHUTDOWN) => + val networkFeeratePerKw = feeratesPerKw.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) d.commitments.localParams.isFunder match { case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) @@ -1169,7 +1169,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).map(Satoshi) val nextClosingFee = Closing.nextClosingFee( - localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), + localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.feeTargets)), remoteClosingFee = Satoshi(remoteClosingFee)) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (Some(nextClosingFee) == lastLocalClosingFee) { @@ -1214,19 +1214,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") val localCommitPublished1 = d.localCommitPublished.map { case localCommitPublished => - val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx) + val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.feeTargets) doPublish(localCommitPublished1) localCommitPublished1 } val remoteCommitPublished1 = d.remoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { case remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) + val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished1) remoteCommitPublished1 } @@ -1551,7 +1551,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them if (d.commitments.localParams.isFunder) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.feeTargets) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) goto(NEGOTIATING) using store(d.copy(closingTxProposed = closingTxProposed1)) sending d.localShutdown :: closingSigned :: Nil } else { @@ -1906,7 +1906,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else { val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.feeTargets) doPublish(localCommitPublished) val nextData = d match { @@ -1981,7 +1981,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -1998,7 +1998,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") // if we are in this state, then this field is defined val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint.get - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(nodeParams.feeTargets, keyManager, d.commitments, remotePerCommitmentPoint, commitTx) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) doPublish(remoteCommitPublished) @@ -2011,7 +2011,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val remoteCommit = d.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit require(commitTx.txid == remoteCommit.txid, "txid mismatch") - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx) + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.feeTargets) doPublish(remoteCommitPublished) val nextData = d match { @@ -2044,7 +2044,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { log.warning(s"funding tx spent in txid=${tx.txid}") - Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels) match { + Helpers.Closing.claimRevokedRemoteCommitTxOutputs(nodeParams.feeTargets, keyManager, d.commitments, tx, nodeParams.db.channels) match { case Some(revokedCommitPublished) => log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") val exc = FundingTxSpent(d.channelId, tx) @@ -2091,7 +2091,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's try to spend our current local tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx) + val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.feeTargets) doPublish(localCommitPublished) goto(ERR_INFORMATION_LEAK) sending error diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/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 fd044ee1c8..6876775e89 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -310,7 +310,7 @@ object Commitments { (commitments1, fee) } - def receiveFee(commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { + def receiveFee(feeTargets: FeeTargets, commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = { if (commitments.localParams.isFunder) { throw FundeeCannotSendUpdateFee(commitments.channelId) } @@ -319,7 +319,7 @@ object Commitments { throw FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val localFeeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.commitmentBlockTarget) if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) { throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index f4609344f5..caff890a55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -111,7 +111,7 @@ object Helpers { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } - val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 + val localFeeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) // only enforce dust limit check on mainnet if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { @@ -423,21 +423,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, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Satoshi = { + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + val blockTarget = Math.max(feeTargets.mutualCloseBlockTarget, feeTargets.commitmentBlockTarget) + val feeratePerKw = Globals.feeratesPerKw.get.getRatePerBlock(blockTarget) + 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, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeTargets) makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee) } @@ -490,7 +495,7 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @return a list of transactions (one per HTLC that we can claim) */ - def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { + def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") @@ -498,8 +503,7 @@ object Helpers { val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) - // no need to use a high fee rate for delayed transactions (we are the only one who can spend them) - val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwDelayed = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { @@ -560,9 +564,10 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) * @param tx the remote commitment transaction that has just been published + * @param feeTargets the block target to use when computing fees for the transactions * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { import commitments.{commitInput, localParams, remoteParams} require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) @@ -602,7 +607,7 @@ object Helpers { }) }.toSeq.flatten - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( + claimRemoteCommitMainOutput(feeTargets, keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } ) @@ -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(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - // no need to use a high fee rate for our main output (we are the only one who can spend it) - val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwMain = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output")(Try { val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), @@ -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(feeTargets: FeeTargets, keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) @@ -669,8 +673,7 @@ object Helpers { val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - // no need to use a high fee rate for our main output (we are the only one who can spend it) - val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 + val feeratePerKwMain = Globals.feeratesPerKw.get.getRatePerBlock(feeTargets.claimMainBlockTarget) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty val feeratePerKwPenalty = Globals.feeratesPerKw.get.blocks_2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/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..b6eeccf7dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -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 = Globals.feeratesPerKw.get().getRatePerBlock(nodeParams.feeTargets.commitmentBlockTarget) + val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.getRatePerBlock(nodeParams.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 0febeaab9e..b7b688681d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -65,6 +65,7 @@ object Transactions { case object OutputNotFound extends RuntimeException(s"output not found (probably trimmed)") with TxGenerationSkipped case object AmountBelowDustLimit extends RuntimeException(s"amount is below dust limit") with TxGenerationSkipped + case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) // @formatter:on /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 79770b5833..777f48d693 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -26,8 +26,10 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf +import fr.acinq.eclair.transactions.Transactions.FeeTargets import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector + import scala.concurrent.duration._ /** @@ -64,7 +66,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -74,6 +75,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, + feeTargets = FeeTargets(6, 2, 2, 6), maxFeerateMismatch = 1.5, updateFeeMinDiffRatio = 0.1, autoReconnect = false, @@ -130,7 +132,6 @@ object TestConstants { minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - smartfeeNBlocks = 3, feeBaseMsat = 546000, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) @@ -140,6 +141,7 @@ object TestConstants { pingInterval = 30 seconds, pingTimeout = 10 seconds, pingDisconnect = true, + feeTargets = FeeTargets(6, 2, 2, 6), maxFeerateMismatch = 1.0, updateFeeMinDiffRatio = 0.1, autoReconnect = false, 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..23ed6d88f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala @@ -18,9 +18,13 @@ package fr.acinq.eclair.blockchain.fee import akka.actor.ActorSystem import akka.util.Timeout +import com.softwaremill.sttp.okhttp.{OkHttpBackend, OkHttpFutureBackend} +import fr.acinq.bitcoin.Block import org.json4s.DefaultFormats import org.scalatest.FunSuite +import scala.concurrent.Await + /** * Created by PM on 27/01/2017. */ @@ -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) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index 4f1cfe3b48..9b3f953b07 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -63,7 +63,8 @@ class EarnDotComFeeProviderSpec extends FunSuite with Logging { blocks_6 = 230 * 1000, blocks_12 = 140 * 1000, blocks_36 = 60 * 1000, - blocks_72 = 40 * 1000) + blocks_72 = 40 * 1000, + blocks_144 = 10 * 1000) assert(feerates === ref) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index bf70cdfd02..a742fa6eb2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -43,7 +43,7 @@ class FallbackFeeProviderSpec extends FunSuite { } else Future.failed(new RuntimeException()) } - def dummyFeerates = FeeratesPerKB(1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000)) + def dummyFeerates = FeeratesPerKB(1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000)) def await[T](f: Future[T]): T = Await.result(f, 3 seconds) @@ -73,9 +73,9 @@ class FallbackFeeProviderSpec extends FunSuite { } test("ensure minimum feerate") { - val constantFeeProvider = new ConstantFeeProvider(FeeratesPerKB(1000, 1000, 1000, 1000, 1000, 1000)) + val constantFeeProvider = new ConstantFeeProvider(FeeratesPerKB(1000, 1000, 1000, 1000, 1000, 1000, 1000)) val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, 2) - assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(2000, 2000, 2000, 2000, 2000, 2000)) + assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(2000, 2000, 2000, 2000, 2000, 2000, 1000)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 6c0e514ef4..a38546dc3b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -26,11 +26,11 @@ import scala.concurrent.{Await, Future} class SmoothFeeProviderSpec extends FunSuite { test("smooth fee rates") { val rates = Array( - FeeratesPerKB(100, 200, 300, 400, 500, 600), - FeeratesPerKB(200, 300, 400, 500, 600, 700), - FeeratesPerKB(300, 400, 500, 600, 700, 800), - FeeratesPerKB(300, 400, 500, 600, 700, 800), - FeeratesPerKB(300, 400, 500, 600, 700, 800) + FeeratesPerKB(100, 200, 300, 400, 500, 600, 650), + FeeratesPerKB(200, 300, 400, 500, 600, 700, 750), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850), + FeeratesPerKB(300, 400, 500, 600, 700, 800, 850) ) val provider = new FeeProvider { var index = 0 @@ -55,7 +55,7 @@ class SmoothFeeProviderSpec extends FunSuite { assert(rate1 == rates(0)) assert(rate2 == SmoothFeeProvider.smooth(Seq(rates(0), rates(1)))) assert(rate3 == SmoothFeeProvider.smooth(Seq(rates(0), rates(1), rates(2)))) - assert(rate3 == FeeratesPerKB(200, 300, 400, 500, 600, 700)) + assert(rate3 == FeeratesPerKB(200, 300, 400, 500, 600, 700, 750)) assert(rate4 == SmoothFeeProvider.smooth(Seq(rates(1), rates(2), rates(3)))) assert(rate5 == rates(4)) // since the last 3 values are the same } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index b37eb9418b..023a3ab348 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{htlcSuccessWeight, htlcTimeoutWeight, weight2fee} -import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.transactions.{IN, OUT, Transactions} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} @@ -680,7 +680,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) - alice2bob.expectMsgType[UpdateFee] + val updateFee = alice2bob.expectMsgType[UpdateFee] + assert(updateFee.feeratePerKw == TestConstants.feeratePerKw + 1000) alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1403,11 +1404,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ + Globals.feeratesPerKw.set(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000, 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 = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget) + assert(localFeeRate === 2000) + val remoteFeeUpdate = 85000 + sender.send(bob, UpdateFee(ByteVector32.Zeroes, remoteFeeUpdate)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000") + assert(new String(error.data.toArray) === s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeUpdate localFeeratePerKw=$localFeeRate") awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -1420,6 +1426,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw == Globals.feeratesPerKw.get().getRatePerBlock(Bob.nodeParams.feeTargets.commitmentBlockTarget)) sender.send(bob, UpdateFee(ByteVector32.Zeroes, 252)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") @@ -1685,9 +1692,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200, 14400)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => @@ -1749,6 +1756,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // in response to that, alice publishes its claim txes val claimTxes = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx + val claimMain = claimTxes(0) // in addition to its main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { assert(claimHtlcTx.txIn.size == 1) @@ -1760,7 +1768,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(amountClaimed == Satoshi(814880)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimMain)) // claim-main assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) @@ -1770,6 +1778,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) + + // assert the feerate of the claim main is what we expect + val expectedFeeRate = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.claimMainBlockTarget) + val expectedFee = Transactions.weight2fee(expectedFeeRate, 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 dd73f294f4..dadfad2623 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -20,8 +20,9 @@ import java.util.UUID import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.{PrivateKey} +import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} +import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ @@ -645,9 +646,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) + val event = CurrentFeerates(FeeratesPerKw(100, 200, 600, 1200, 3600, 7200, 14400)) sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget))) } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ec19d818e7..41ed99ff97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -21,10 +21,12 @@ import java.util.UUID import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} +import com.typesafe.sslconfig.util.NoopLogger import fr.acinq.bitcoin.{ByteVector32, OutPoint, ScriptFlags, Transaction, TxIn} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment._ @@ -137,6 +139,26 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // both nodes are now in CLOSING state with a mutual close tx pending for confirmation } + test("start fee negotiation from configured block target") { f => + import f._ + + Globals.feeratesPerKw.set(FeeratesPerKw(100, 250, 350, 450, 600, 800, 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 = Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.mutualCloseBlockTarget) + val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) + assert(Alice.nodeParams.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == 250) + assert(closing.feeSatoshis == expectedFirstProposedFee.amount) + } + test("recv BITCOIN_FUNDING_PUBLISH_FAILED", Tag("funding_unconfirmed")) { f => import f._ alice ! CMD_FORCECLOSE 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..45a0f31727 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,11 @@ 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.{ChannelCreated, Data, HasCommitments, State} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo @@ -65,7 +66,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 +238,22 @@ class PeerSpec extends TestkitBaseClass { probe.expectMsg("disconnecting") } + test("use correct fee rates when spawning a channel") { f => + import f._ + + val probe = TestProbe() + system.eventStream.subscribe(probe.ref, classOf[ChannelCreated]) + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + + assert(peer.stateData.channels.isEmpty) + probe.send(peer, Peer.OpenChannel(remoteNodeId, Satoshi(12300), MilliSatoshi(0), None, None, None)) + awaitCond(peer.stateData.channels.nonEmpty) + + val channelCreated = probe.expectMsgType[ChannelCreated] + assert(channelCreated.initialFeeratePerKw == Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.commitmentBlockTarget)) + assert(channelCreated.fundingTxFeeratePerKw.get == Globals.feeratesPerKw.get.getRatePerBlock(Alice.nodeParams.feeTargets.fundingBlockTarget)) + } + test("reply to ping") { f => import f._ val probe = TestProbe() diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index d63969c9b0..ed96e755f0 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -75,7 +75,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging def main(m: Map[ActorRef, ChannelPaneController]): Receive = { - case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId) => + case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId, _, _) => context.watch(channel) val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, temporaryChannelId) runInGuiThread(() => mainController.channelBox.getChildren.addAll(root))