Skip to content

Commit d735e0b

Browse files
authored
Improve channel and payment events (#3246)
We improve the following events: - `TransactionPublished` includes more details about mining fees and an optional liquidity purchase - all channel events include the latest `channel_type` - `PaymentRelayed` exposes the `relayFee` earned
1 parent 369f042 commit d735e0b

File tree

19 files changed

+95
-93
lines changed

19 files changed

+95
-93
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction, TxId}
2222
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2323
import fr.acinq.eclair.channel.Helpers.Closing.ClosingType
24+
import fr.acinq.eclair.transactions.Transactions
2425
import fr.acinq.eclair.wire.protocol._
25-
import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshi, RealShortChannelId, ShortChannelId}
26+
import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli}
2627

2728
/**
2829
* Created by PM on 17/08/2016.
@@ -92,10 +93,19 @@ case class ChannelLiquidityPurchased(channel: ActorRef, channelId: ByteVector32,
9293

9394
case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, error: ChannelError, isFatal: Boolean) extends ChannelEvent
9495

95-
// NB: the fee should be set to 0 when we're not paying it.
96-
case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction, miningFee: Satoshi, desc: String) extends ChannelEvent
96+
/**
97+
* We published a transaction related to the given [[channelId]].
98+
*
99+
* @param localMiningFee mining fee paid by us in the given [[tx]].
100+
* @param remoteMiningFee mining fee paid by our channel peer in the given [[tx]].
101+
* @param liquidityPurchase_opt optional liquidity purchase included in this transaction.
102+
*/
103+
case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction, localMiningFee: Satoshi, remoteMiningFee: Satoshi, desc: String, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo], timestamp: TimestampMilli = TimestampMilli.now()) extends ChannelEvent {
104+
val miningFee: Satoshi = localMiningFee + remoteMiningFee
105+
val feerate: FeeratePerKw = Transactions.fee2rate(miningFee, tx.weight())
106+
}
97107

98-
case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction) extends ChannelEvent
108+
case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction, timestamp: TimestampMilli = TimestampMilli.now()) extends ChannelEvent
99109

100110
// NB: this event is only sent when the channel is available.
101111
case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, aliases: ShortIdAliases, commitments: Commitments, lastAnnouncement_opt: Option[ChannelAnnouncement]) extends ChannelEvent

eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -971,11 +971,12 @@ object Helpers {
971971
}
972972
}
973973

974-
/** Compute the fee paid by a commitment transaction. */
975-
def commitTxFee(commitInput: InputInfo, commitTx: Transaction, localPaysCommitTxFees: Boolean): Satoshi = {
974+
/** Compute the fee paid by a commitment transaction. The first result is the fee paid by us, the second one is the fee paid by our peer. */
975+
def commitTxFee(commitInput: InputInfo, commitTx: Transaction, localPaysCommitTxFees: Boolean): (Satoshi, Satoshi) = {
976976
require(commitTx.txIn.size == 1, "transaction must have only one input")
977977
require(commitTx.txIn.exists(txIn => txIn.outPoint == commitInput.outPoint), "transaction must spend the funding output")
978-
if (localPaysCommitTxFees) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat
978+
val commitFee = commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum
979+
if (localPaysCommitTxFees) (commitFee, 0 sat) else (0 sat, commitFee)
979980
}
980981

981982
/** Return the confirmation target that should be used for our local commitment. */

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ trait DualFundingHandlers extends CommonFundingHandlers {
5151
// to publish and we may be able to RBF.
5252
wallet.publishTransaction(fundingTx.signedTx).onComplete {
5353
case Success(_) =>
54-
context.system.eventStream.publish(TransactionPublished(dualFundedTx.fundingParams.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.localFees.truncateToSatoshi, "funding"))
54+
context.system.eventStream.publish(TransactionPublished(dualFundedTx.fundingParams.channelId, remoteNodeId, fundingTx.signedTx, localMiningFee = fundingTx.tx.localFees.truncateToSatoshi, remoteMiningFee = fundingTx.tx.remoteFees.truncateToSatoshi, "funding", dualFundedTx.liquidityPurchase_opt))
5555
// We rely on Bitcoin Core ZMQ notifications to learn about transactions that appear in our mempool, but
5656
// it doesn't provide strong guarantees that we'll always receive an event. This can be an issue for 0-conf
5757
// funding transactions, where we end up delaying our channel_ready or splice_locked.

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ trait ErrorHandlers extends CommonHandlers {
232232

233233
/** Publish 2nd-stage transactions for our local commitment. */
234234
def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = {
235-
val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees), None)
235+
val (localFee, _) = Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees)
236+
val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", localFee, None)
236237
val publishAnchorTx_opt = txs.anchorTx_opt match {
237238
case Some(anchorTx) if !lcp.isConfirmed =>
238239
val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf)
@@ -274,7 +275,8 @@ trait ErrorHandlers extends CommonHandlers {
274275
case closing: DATA_CLOSING => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, closing.maxClosingFeerate_opt)
275276
case _ => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None)
276277
}
277-
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "remote-commit"))
278+
val (localFee, remoteFee) = Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees)
279+
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, localFee, remoteFee, "remote-commit", None))
278280
val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
279281
val nextData = d match {
280282
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
@@ -296,7 +298,8 @@ trait ErrorHandlers extends CommonHandlers {
296298
case closing: DATA_CLOSING => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, closing.maxClosingFeerate_opt)
297299
case _ => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None)
298300
}
299-
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "next-remote-commit"))
301+
val (localFee, remoteFee) = Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees)
302+
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, localFee, remoteFee, "next-remote-commit", None))
300303
val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs)
301304
val nextData = d match {
302305
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
@@ -350,7 +353,8 @@ trait ErrorHandlers extends CommonHandlers {
350353
val dustLimit = commitment.localCommitParams.dustLimit
351354
val (revokedCommitPublished, closingTxs) = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.channelParams, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
352355
log.warning("txid={} was a revoked commitment, publishing the penalty tx", tx.txid)
353-
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "revoked-commit"))
356+
val (localFee, remoteFee) = Closing.commitTxFee(commitment.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees)
357+
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, localFee, remoteFee, "revoked-commit", None))
354358
val exc = FundingTxSpent(d.channelId, tx.txid)
355359
val error = Error(d.channelId, exc.getMessage)
356360
val nextData = d match {
@@ -364,7 +368,8 @@ trait ErrorHandlers extends CommonHandlers {
364368
case None => d match {
365369
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT =>
366370
log.warning("they published a future commit (because we asked them to) in txid={}", tx.txid)
367-
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "future-remote-commit"))
371+
val (localFee, remoteFee) = Closing.commitTxFee(d.commitments.latest.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees)
372+
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, localFee, remoteFee, "future-remote-commit", None))
368373
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint
369374
val commitKeys = d.commitments.latest.remoteKeys(channelKeys, remotePerCommitmentPoint)
370375
val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None)

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ trait SingleFundingHandlers extends CommonFundingHandlers {
4343
def publishFundingTx(channelId: ByteVector32, fundingTx: Transaction, fundingTxFee: Satoshi, replyTo: akka.actor.typed.ActorRef[OpenChannelResponse]): Unit = {
4444
wallet.commit(fundingTx).onComplete {
4545
case Success(true) =>
46-
context.system.eventStream.publish(TransactionPublished(channelId, remoteNodeId, fundingTx, fundingTxFee, "funding"))
46+
context.system.eventStream.publish(TransactionPublished(channelId, remoteNodeId, fundingTx, localMiningFee = fundingTxFee, remoteMiningFee = 0 sat, "funding", None))
4747
replyTo ! OpenChannelResponse.Created(channelId, fundingTxId = fundingTx.txid, fundingTxFee)
4848
case Success(false) =>
4949
replyTo ! OpenChannelResponse.Rejected("couldn't publish funding tx")

eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish
1919
import akka.actor.typed.eventstream.EventStream
2020
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
2121
import akka.actor.typed.{ActorRef, Behavior}
22-
import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction, TxId}
22+
import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction, TxId}
2323
import fr.acinq.eclair.blockchain.CurrentBlockHeight
2424
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
2525
import fr.acinq.eclair.channel.publish.TxPublisher.{TxPublishContext, TxRejectedReason}
@@ -136,7 +136,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams,
136136

137137
private def waitForConfirmation(): Behavior[Command] = {
138138
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[CurrentBlockHeight](cbc => WrappedCurrentBlockHeight(cbc.blockHeight)))
139-
context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc))
139+
context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, localMiningFee = cmd.fee, remoteMiningFee = 0 sat, cmd.desc, None))
140140
Behaviors.receiveMessagePartial {
141141
case WrappedCurrentBlockHeight(currentBlockHeight) =>
142142
timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)

eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._
2929
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
3030
import fr.acinq.eclair.payment.Monitoring.{Metrics => PaymentMetrics, Tags => PaymentTags}
3131
import fr.acinq.eclair.payment._
32-
import fr.acinq.eclair.{Logs, NodeParams}
32+
import fr.acinq.eclair.{Logs, NodeParams, TimestampMilli}
3333

3434
/**
3535
* This actor sits at the interface between our event stream and the database.
@@ -90,8 +90,8 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
9090
incoming.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived))
9191
outgoing.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentSent))
9292
case ChannelPaymentRelayed(_, incoming, outgoing) =>
93-
channelsDb.updateChannelMeta(incoming.channelId, ChannelEvent.EventType.PaymentReceived)
94-
channelsDb.updateChannelMeta(outgoing.channelId, ChannelEvent.EventType.PaymentSent)
93+
incoming.foreach(i => channelsDb.updateChannelMeta(i.channelId, ChannelEvent.EventType.PaymentReceived))
94+
outgoing.foreach(o => channelsDb.updateChannelMeta(o.channelId, ChannelEvent.EventType.PaymentSent))
9595
case OnTheFlyFundingPaymentRelayed(_, incoming, outgoing) =>
9696
incoming.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived))
9797
outgoing.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentSent))
@@ -124,7 +124,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
124124
case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY | WAIT_FOR_DUAL_FUNDING_READY, NORMAL, Some(commitments)) =>
125125
ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment()
126126
val event = ChannelEvent.EventType.Created
127-
auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.fundingTxId, commitments.latest.capacity, commitments.localChannelParams.isChannelOpener, !commitments.announceChannel, event))
127+
auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.fundingTxId, commitments.latest.commitmentFormat.toString, commitments.latest.capacity, commitments.localChannelParams.isChannelOpener, !commitments.announceChannel, event.label))
128128
channelsDb.updateChannelMeta(channelId, event)
129129
case ChannelStateChanged(_, channelId, _, _, OFFLINE, SYNCING, _) =>
130130
channelsDb.updateChannelMeta(channelId, ChannelEvent.EventType.Connected)
@@ -141,7 +141,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
141141
case 0 => ChannelEvent.EventType.Confirmed
142142
case _ => ChannelEvent.EventType.Spliced
143143
}
144-
auditDb.add(ChannelEvent(e.channelId, e.remoteNodeId, e.fundingTxId, e.commitments.latest.capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event))
144+
auditDb.add(ChannelEvent(e.channelId, e.remoteNodeId, e.fundingTxId, e.commitments.latest.commitmentFormat.toString, e.commitments.latest.capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event.label))
145145

146146
case e: ChannelClosed =>
147147
ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Closed).increment()
@@ -150,7 +150,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL
150150
// spent by the closing transaction.
151151
val capacity = e.commitments.latest.capacity
152152
val fundingTxId = e.commitments.latest.fundingTxId
153-
auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteNodeId, fundingTxId, capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event))
153+
auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteNodeId, fundingTxId, e.commitments.latest.commitmentFormat.toString, capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event.label))
154154
channelsDb.updateChannelMeta(e.channelId, event)
155155

156156
case u: ChannelUpdateParametersChanged =>
@@ -178,7 +178,7 @@ object DbEventHandler {
178178
def props(nodeParams: NodeParams): Props = Props(new DbEventHandler(nodeParams))
179179

180180
// @formatter:off
181-
case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, capacity: Satoshi, isChannelOpener: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType)
181+
case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, channelType: String, capacity: Satoshi, isChannelOpener: Boolean, isPrivate: Boolean, event: String, timestamp: TimestampMilli = TimestampMilli.now())
182182
object ChannelEvent {
183183
sealed trait EventType { def label: String }
184184
object EventType {
@@ -190,12 +190,12 @@ object DbEventHandler {
190190
object PaymentReceived extends EventType { override def label: String = "received" }
191191
case class Closed(closingType: ClosingType) extends EventType {
192192
override def label: String = closingType match {
193-
case _: MutualClose => "mutual"
194-
case _: LocalClose => "local"
195-
case _: CurrentRemoteClose => "remote"
196-
case _: NextRemoteClose => "remote"
197-
case _: RecoveryClose => "recovery"
198-
case _: RevokedClose => "revoked"
193+
case _: MutualClose => "mutual-close"
194+
case _: LocalClose => "local-close"
195+
case _: CurrentRemoteClose => "remote-close"
196+
case _: NextRemoteClose => "remote-close"
197+
case _: RecoveryClose => "recovery-close"
198+
case _: RevokedClose => "revoked-close"
199199
}
200200
}
201201
}

0 commit comments

Comments
 (0)