What changed, and why it matters
This commit fixes how the Eclair Lightning node compares its on-chain Bitcoin funds (UTXOs) when computing balance changes. Previously, it compared entire UTXO objects, which include fields that can change even when the same coin is still owned (for example, confirmation count). This could cause the node to incorrectly think UTXOs were added or removed, producing noisy or misleading balance logs and metrics. The fix compares UTXOs only by their unique identifier (outPoint), which is the correct way to tell whether the same coin is still present. There is no direct evidence in the commit that this is exploitable by an attacker or that it causes loss of funds.
Treat as a routine correctness/logging fix. Review whether any downstream alerting, accounting, or channel-force-close logic consumes these UTXO diff metrics and could react to the previously spurious add/remove events. No urgent security patch is indicated by the diff alone.
Security signals we found
Incorrect equality/comparison of financial objects could inflate or suppress balance-change metrics
Fix moves from full-object Set comparison to stable outPoint-keyed Map comparison
No input validation, serialization, cryptography, or authorization changes observed
No evidence of remote attacker control over affected data path
Evidence from the diff
BalanceActor previously computed added/removed UTXOs by converting full Utxo case-class instances to a Set and using set difference. Because Utxo contains mutable-ish fields such as blockHeight and confirmations, two representations of the same outPoint could compare unequal, causing false positives in utxosAdded/utxosRemoved. The patch indexes UTXOs by outPoint (a TxHash + index pair) before differencing, so only genuinely new or spent outPoints are reported. The change is local to metric/logging logic and does not alter transaction signing, coin selection, or wallet state.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scalaInspect captured patch +4 / −4
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala
index c2805be..667ba1c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala
@@ -106,10 +106,10 @@ private class BalanceActor(context: ActorContext[Command],
case Some(previousBalance) =>
// On-chain metrics:
log.info("on-chain diff={}", balance.onChain.total - previousBalance.onChain.total)
- val utxosBefore = previousBalance.onChain.utxos.toSet
- val utxosAfter = balance.onChain.utxos.toSet
- val utxosAdded = utxosAfter -- utxosBefore
- val utxosRemoved = utxosBefore -- utxosAfter
+ val utxosBefore = previousBalance.onChain.utxos.map(utxo => utxo.outPoint -> utxo).toMap
+ val utxosAfter = balance.onChain.utxos.map(utxo => utxo.outPoint -> utxo).toMap
+ val utxosAdded = (utxosAfter -- utxosBefore.keys).values
+ val utxosRemoved = (utxosBefore -- utxosAfter.keys).values
utxosAdded
.toList.sortBy(_.amount)
.foreach(utxo => log.info("+ utxo={} amount={}", utxo.outPoint, utxo.amount))
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.