1+ /*
2+ * Copyright 2024 ACINQ SAS
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
117package fr .acinq .eclair .balance
218
319import akka .actor .typed .eventstream .EventStream
@@ -7,12 +23,11 @@ import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong}
723import fr .acinq .eclair .NotificationsLogger
824import fr .acinq .eclair .NotificationsLogger .NotifyNodeOperator
925import fr .acinq .eclair .balance .BalanceActor ._
10- import fr .acinq .eclair .balance .CheckBalance .{GlobalBalance , computeOffChainBalance }
26+ import fr .acinq .eclair .balance .CheckBalance .{GlobalBalance , OffChainBalance }
1127import fr .acinq .eclair .balance .Monitoring .{Metrics , Tags }
1228import fr .acinq .eclair .blockchain .bitcoind .rpc .BitcoinCoreClient
1329import fr .acinq .eclair .blockchain .bitcoind .rpc .BitcoinCoreClient .Utxo
1430import fr .acinq .eclair .channel .PersistentChannelData
15- import fr .acinq .eclair .db .Databases
1631
1732import scala .concurrent .ExecutionContext
1833import scala .concurrent .duration ._
@@ -29,26 +44,26 @@ object BalanceActor {
2944 private final case class WrappedGlobalBalanceWithChannels (wrapped : Try [GlobalBalance ], channelsCount : Int ) extends Command
3045 // @formatter:on
3146
32- def apply (db : Databases , bitcoinClient : BitcoinCoreClient , channelsListener : ActorRef [ChannelsListener .GetChannels ], interval : FiniteDuration )(implicit ec : ExecutionContext ): Behavior [Command ] = {
47+ def apply (bitcoinClient : BitcoinCoreClient , minDepth : Int , channelsListener : ActorRef [ChannelsListener .GetChannels ], interval : FiniteDuration )(implicit ec : ExecutionContext ): Behavior [Command ] = {
3348 Behaviors .setup { context =>
3449 Behaviors .withTimers { timers =>
3550 timers.startTimerWithFixedDelay(TickBalance , interval)
36- new BalanceActor (context, db, bitcoinClient , channelsListener).apply(refBalance_opt = None , previousBalance_opt = None )
51+ new BalanceActor (context, bitcoinClient, minDepth , channelsListener).apply(refBalance_opt = None , previousBalance_opt = None )
3752 }
3853 }
3954 }
4055
4156}
4257
4358private class BalanceActor (context : ActorContext [Command ],
44- db : Databases ,
4559 bitcoinClient : BitcoinCoreClient ,
60+ minDepth : Int ,
4661 channelsListener : ActorRef [ChannelsListener .GetChannels ])(implicit ec : ExecutionContext ) {
4762
4863 private val log = context.log
4964
5065 /**
51- * @param refBalance_opt the reference balance computed once at startup, useful for telling if we are making or losing money overall
66+ * @param refBalance_opt the reference balance computed once at startup, useful for telling if we are making or losing money overall
5267 * @param previousBalance_opt the last computed balance, it is useful to make a detailed diff between two successive balance checks
5368 * @return
5469 */
@@ -65,7 +80,7 @@ private class BalanceActor(context: ActorContext[Command],
6580 Behaviors .same
6681 case WrappedChannels (res) =>
6782 val channelsCount = res.channels.size
68- context.pipeToSelf(CheckBalance .computeGlobalBalance(res.channels, db, bitcoinClient ))(b => WrappedGlobalBalanceWithChannels (b, channelsCount))
83+ context.pipeToSelf(CheckBalance .computeGlobalBalance(res.channels, bitcoinClient, minDepth ))(b => WrappedGlobalBalanceWithChannels (b, channelsCount))
6984 Behaviors .same
7085 case WrappedGlobalBalanceWithChannels (res, channelsCount) =>
7186 res match {
@@ -89,61 +104,61 @@ private class BalanceActor(context: ActorContext[Command],
89104 }
90105 previousBalance_opt match {
91106 case Some (previousBalance) =>
107+ // On-chain metrics:
92108 log.info(" on-chain diff={}" , balance.onChain.total - previousBalance.onChain.total)
93- val utxosBefore = previousBalance.onChain.confirmed ++ previousBalance.onChain.unconfirmed
94- val utxosAfter = balance.onChain.confirmed ++ balance.onChain.unconfirmed
95- val utxosAdded = utxosAfter -- utxosBefore.keys
96- val utxosRemoved = utxosBefore -- utxosAfter.keys
109+ val utxosBefore = previousBalance.onChain.utxos.toSet
110+ val utxosAfter = balance.onChain.utxos.toSet
111+ val utxosAdded = utxosAfter -- utxosBefore
112+ val utxosRemoved = utxosBefore -- utxosAfter
97113 utxosAdded
98- .toList.sortBy(- _._2 )
99- .foreach { case (outPoint, amount) => log.info(" + utxo={} amount={}" , outPoint, amount) }
114+ .toList.sortBy(_.amount )
115+ .foreach(utxo => log.info(" + utxo={} amount={}" , utxo. outPoint, utxo. amount))
100116 utxosRemoved
101- .toList.sortBy(- _._2 )
102- .foreach { case (outPoint, amount) => log.info(" - utxo={} amount={}" , outPoint, amount) }
103-
117+ .toList.sortBy(_.amount )
118+ .foreach(utxo => log.info(" - utxo={} amount={}" , utxo. outPoint, utxo. amount))
119+ // Off-chain metrics:
104120 log.info(" off-chain diff={}" , balance.offChain.total - previousBalance.offChain.total)
105- val offchainBalancesBefore = previousBalance.channels.view.mapValues(computeOffChainBalance(previousBalance.knownPreimages, _ ).total)
106- val offchainBalancesAfter = balance.channels.view.mapValues(computeOffChainBalance(balance.knownPreimages, _ ).total)
107- offchainBalancesAfter
108- .map { case (channelId, balanceAfter) => (channelId, balanceAfter - offchainBalancesBefore .getOrElse(channelId, Btc (0 ))) }
121+ val offChainBalancesBefore = previousBalance.channels.view.mapValues(channel => OffChainBalance ().addChannelBalance(channel ).total)
122+ val offChainBalancesAfter = balance.channels.view.mapValues(channel => OffChainBalance ().addChannelBalance(channel ).total)
123+ offChainBalancesAfter
124+ .map { case (channelId, balanceAfter) => (channelId, balanceAfter - offChainBalancesBefore .getOrElse(channelId, Btc (0 ))) }
109125 .filter { case (_, balanceDiff) => balanceDiff > 0 .sat }
110126 .toList.sortBy(- _._2)
111127 .foreach { case (channelId, balanceDiff) => log.info(" + channelId={} amount={}" , channelId, balanceDiff) }
112- offchainBalancesBefore
113- .map { case (channelId, balanceBefore) => (channelId, balanceBefore - offchainBalancesAfter .getOrElse(channelId, Btc (0 ))) }
128+ offChainBalancesBefore
129+ .map { case (channelId, balanceBefore) => (channelId, balanceBefore - offChainBalancesAfter .getOrElse(channelId, Btc (0 ))) }
114130 .filter { case (_, balanceDiff) => balanceDiff > 0 .sat }
115131 .toList.sortBy(- _._2)
116132 .foreach { case (channelId, balanceDiff) => log.info(" - channelId={} amount={}" , channelId, balanceDiff) }
117133 case None => ()
118134 }
119-
120- log.info(" current balance: total={} onchain.confirmed={} onchain.unconfirmed={} offchain={}" , balance.total.toDouble, balance.onChain.totalConfirmed.toDouble, balance.onChain.totalUnconfirmed.toDouble, balance.offChain.total.toDouble)
135+ log.info(" current balance: total={} onchain.deeply-confirmed={} onchain.recently-confirmed={} onchain.unconfirmed={} offchain={}" , balance.total.toDouble, balance.onChain.totalDeeplyConfirmed.toDouble, balance.onChain.totalRecentlyConfirmed.toDouble, balance.onChain.totalUnconfirmed.toDouble, balance.offChain.total.toDouble)
121136 log.debug(" current balance details: {}" , balance)
122137 // This is a very rough estimation of the fee we would need to pay for a force-close with 5 pending HTLCs at 100 sat/byte.
123138 val perChannelFeeBumpingReserve = 50_000 .sat
124139 // Instead of scaling this linearly with the number of channels we have, we use sqrt(channelsCount) to reflect
125140 // the fact that if you have channels with many peers, only a subset of these peers will likely be malicious.
126141 val estimatedFeeBumpingReserve = perChannelFeeBumpingReserve * Math .sqrt(channelsCount)
127- if (balance.onChain.totalConfirmed < estimatedFeeBumpingReserve) {
142+ val totalConfirmedBalance = balance.onChain.totalDeeplyConfirmed + balance.onChain.totalRecentlyConfirmed
143+ if (totalConfirmedBalance < estimatedFeeBumpingReserve) {
128144 val message =
129- s """ On-chain confirmed balance is low ( ${balance.onChain.totalConfirmed .toMilliBtc}): eclair may not be able to guarantee funds safety in case channels force-close.
145+ s """ On-chain confirmed balance is low ( ${totalConfirmedBalance .toMilliBtc}): eclair may not be able to guarantee funds safety in case channels force-close.
130146 |You have $channelsCount channels, which could cost $estimatedFeeBumpingReserve in fees if some of these channels are malicious.
131147 |Please note that the value above is a very arbitrary estimation: the real cost depends on the feerate and the number of malicious channels.
132148 |You should add more utxos to your bitcoin wallet to guarantee funds safety.
133149 | """ .stripMargin
134150 context.system.eventStream ! EventStream .Publish (NotifyNodeOperator (NotificationsLogger .Warning , message))
135151 }
136152 Metrics .GlobalBalance .withoutTags().update(balance.total.toMilliBtc.toDouble)
137- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OnchainConfirmed ).update(balance.onChain.totalConfirmed.toMilliBtc.toDouble)
138- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OnchainUnconfirmed ).update(balance.onChain.totalUnconfirmed.toMilliBtc.toDouble)
139- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .waitForFundingConfirmed).update(balance.offChain.waitForFundingConfirmed.toMilliBtc.toDouble)
140- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .waitForChannelReady).update(balance.offChain.waitForChannelReady.toMilliBtc.toDouble)
141- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .normal).update(balance.offChain.normal.total.toMilliBtc.toDouble)
142- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .shutdown).update(balance.offChain.shutdown.total.toMilliBtc.toDouble)
143- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .closingLocal).update(balance.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble)
144- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .closingRemote).update(balance.offChain.closing.remoteCloseBalance.total.toMilliBtc.toDouble)
145- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .closingUnknown).update(balance.offChain.closing.unknownCloseBalance.total.toMilliBtc.toDouble)
146- Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .Offchain ).withTag(Tags .OffchainState , Tags .OffchainStates .waitForPublishFutureCommitment).update(balance.offChain.waitForPublishFutureCommitment.toMilliBtc.toDouble)
153+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OnChainDeeplyConfirmed ).update(balance.onChain.totalDeeplyConfirmed.toMilliBtc.toDouble)
154+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OnChainRecentlyConfirmed ).update(balance.onChain.totalRecentlyConfirmed.toMilliBtc.toDouble)
155+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OnChainUnconfirmed ).update(balance.onChain.totalUnconfirmed.toMilliBtc.toDouble)
156+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .waitForFundingConfirmed).update(balance.offChain.waitForFundingConfirmed.toMilliBtc.toDouble)
157+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .waitForChannelReady).update(balance.offChain.waitForChannelReady.toMilliBtc.toDouble)
158+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .normal).update(balance.offChain.normal.total.toMilliBtc.toDouble)
159+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .shutdown).update(balance.offChain.shutdown.total.toMilliBtc.toDouble)
160+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .closing).update(balance.offChain.closing.total.toMilliBtc.toDouble)
161+ Metrics .GlobalBalanceDetailed .withTag(Tags .BalanceType , Tags .BalanceTypes .OffChain ).withTag(Tags .OffChainState , Tags .OffChainStates .waitForPublishFutureCommitment).update(balance.offChain.waitForPublishFutureCommitment.toMilliBtc.toDouble)
147162 refBalance_opt match {
148163 case Some (refBalance) =>
149164 val normalizedValue = 100 + (if (refBalance.total.toSatoshi.toLong > 0 ) (balance.total.toSatoshi.toLong - refBalance.total.toSatoshi.toLong) * 1000D / refBalance.total.toSatoshi.toLong else 0 )
@@ -162,7 +177,7 @@ private class BalanceActor(context: ActorContext[Command],
162177 Behaviors .same
163178 }
164179 case GetGlobalBalance (replyTo, channels) =>
165- CheckBalance .computeGlobalBalance(channels, db, bitcoinClient ) onComplete (replyTo ! _)
180+ CheckBalance .computeGlobalBalance(channels, bitcoinClient, minDepth ) onComplete (replyTo ! _)
166181 Behaviors .same
167182 }
168183}
0 commit comments