Skip to content

Commit 6bb7f25

Browse files
authored
Add liquidity ads metrics (#3301)
We add metrics for liquidity ads (purchased amount and mining fee refund). We refactor the interactive-tx metrics to record them *after* tx validation, instead of *before*. We also remove the input/output count for the "shared" case, since it's always 0 or 1.
1 parent 113d8fb commit 6bb7f25

2 files changed

Lines changed: 55 additions & 16 deletions

File tree

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package fr.acinq.eclair.channel
1818

1919
import fr.acinq.bitcoin.scalacompat.SatoshiLong
20+
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder
2021
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{InteractiveTxParams, SharedTransaction}
2122
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc}
23+
import fr.acinq.eclair.wire.protocol.LiquidityAds
2224
import kamon.Kamon
2325

2426
object Monitoring {
@@ -33,10 +35,15 @@ object Monitoring {
3335
val HtlcValueInFlightGlobal = Kamon.gauge("channels.htlc-value-in-flight-global", "Global HTLC value in flight across all channels")
3436
val LocalFeeratePerByte = Kamon.histogram("channels.local-feerate-per-byte")
3537
val RemoteFeeratePerByte = Kamon.histogram("channels.remote-feerate-per-byte")
38+
val InteractiveTxFundingTargetAmount = Kamon.histogram("channels.interactive-tx.target-funding-amount", "Interactive tx funding target amount")
39+
val InteractiveTxFundingChangeAmount = Kamon.histogram("channels.interactive-tx.change-amount", "Interactive tx funding change")
40+
val InteractiveTxLocalMiningFee = Kamon.histogram("channels.interactive-tx.local-mining-fee", "Interactive tx mining fee paid by us")
3641
val InteractiveTxInputs = Kamon.histogram("channels.interactive-tx.inputs", "Interactive tx inputs")
3742
val InteractiveTxOutputs = Kamon.histogram("channels.interactive-tx.outputs", "Interactive tx outputs")
3843
val InteractiveTxInputsPerSession = Kamon.histogram("channels.interactive-tx.inputs-per-session", "Interactive tx inputs per session")
3944
val InteractiveTxOutputsPerSession = Kamon.histogram("channels.interactive-tx.outputs-per-session", "Interactive tx outputs per session")
45+
val LiquidityPurchaseAmount = Kamon.histogram("channels.interactive-tx.liquidity-purchase-amount", "Amount of liquidity purchased (if positive) or sold (if negative)")
46+
val LiquidityPurchaseMiningFeeDiff = Kamon.histogram("channels.interactive-tx.liquidity-purchase-mining-fee-diff", "When selling liquidity, difference between the refunded mining fee and the actual mining fee paid")
4047
val Splices = Kamon.histogram("channels.splices", "Splices")
4148
val ProcessMessage = Kamon.timer("channels.messages-processed")
4249
val HtlcDropped = Kamon.counter("channels.htlc-dropped")
@@ -56,6 +63,47 @@ object Monitoring {
5663
}
5764
}
5865

66+
def recordInteractiveTx(fundingParams: InteractiveTxParams, sharedTx: SharedTransaction, liquidityPurchase_opt: Option[LiquidityAds.Purchase]): Unit = {
67+
// Global, not "per session". The goal is to measure the total number of inputs/outputs and distribution of amounts across all interactive-tx sessions.
68+
sharedTx.sharedInput_opt.foreach(i => InteractiveTxInputs.withTag(Tags.InputType, "shared").record(i.txOut.amount.toLong))
69+
sharedTx.localInputs.foreach(i => InteractiveTxInputs.withTag(Tags.InputType, "local").record(i.txOut.amount.toLong))
70+
sharedTx.remoteInputs.foreach(i => InteractiveTxInputs.withTag(Tags.InputType, "remote").record(i.txOut.amount.toLong))
71+
InteractiveTxOutputs.withTag(Tags.OutputType, "shared").record(sharedTx.sharedOutput.amount.toLong)
72+
sharedTx.localOutputs.foreach(o => InteractiveTxOutputs.withTag(Tags.OutputType, "local").record(o.amount.toLong))
73+
sharedTx.remoteOutputs.foreach(o => InteractiveTxOutputs.withTag(Tags.OutputType, "remote").record(o.amount.toLong))
74+
75+
// We measure the number of each non-shared input/output type per session.
76+
if (sharedTx.localInputs.nonEmpty) InteractiveTxInputsPerSession.withTag(Tags.InputType, "local").record(sharedTx.localInputs.size)
77+
if (sharedTx.remoteInputs.nonEmpty) InteractiveTxInputsPerSession.withTag(Tags.InputType, "remote").record(sharedTx.remoteInputs.size)
78+
if (sharedTx.localOutputs.nonEmpty) InteractiveTxOutputsPerSession.withTag(Tags.OutputType, "local").record(sharedTx.localOutputs.size)
79+
if (sharedTx.remoteOutputs.nonEmpty) InteractiveTxOutputsPerSession.withTag(Tags.OutputType, "remote").record(sharedTx.remoteOutputs.size)
80+
81+
// We measure the difference between the amount we want to fund and the resulting change.
82+
if (fundingParams.localContribution >= 0.sat) {
83+
InteractiveTxFundingTargetAmount.withTag(Tags.DiffSign, Tags.DiffSigns.plus).record(fundingParams.localContribution.toLong)
84+
// Note that we explicitly want to record a 0 value when we don't have any change output: it lets us see how many sessions don't need change, which is the best outcome.
85+
InteractiveTxFundingChangeAmount.withoutTags().record(sharedTx.localOutputs.collect { case o: InteractiveTxBuilder.Output.Local.Change => o.amount.toLong }.sum)
86+
} else {
87+
InteractiveTxFundingTargetAmount.withTag(Tags.DiffSign, Tags.DiffSigns.minus).record(-fundingParams.localContribution.toLong)
88+
}
89+
InteractiveTxLocalMiningFee.withoutTags().record(sharedTx.localFees.truncateToSatoshi.toLong)
90+
91+
// We record liquidity purchase details.
92+
liquidityPurchase_opt.foreach(p => {
93+
// If we initiate the interactive-tx session, we're the buyer: otherwise, we're the seller.
94+
LiquidityPurchaseAmount.withTag(Tags.DiffSign, if (fundingParams.isInitiator) Tags.DiffSigns.plus else Tags.DiffSigns.minus).record(p.amount.toLong)
95+
// If the actual mining fee we paid is greater than what the user refunds, we lose money.
96+
if (!fundingParams.isInitiator) {
97+
val miningFeeDiff = p.fees.miningFee - sharedTx.localFees.truncateToSatoshi
98+
if (miningFeeDiff >= 0.sat) {
99+
LiquidityPurchaseMiningFeeDiff.withTag(Tags.DiffSign, Tags.DiffSigns.plus).record(miningFeeDiff.toLong)
100+
} else {
101+
LiquidityPurchaseMiningFeeDiff.withTag(Tags.DiffSign, Tags.DiffSigns.minus).record(-miningFeeDiff.toLong)
102+
}
103+
}
104+
})
105+
}
106+
59107
/**
60108
* This is best effort! It is not possible to attribute a type to a splice in all cases. For example, if remote provides
61109
* both inputs and outputs, it could be a splice-in (with change), or a combined splice-in + splice-out.
@@ -122,6 +170,12 @@ object Monitoring {
122170
val SpliceOut = "splice-out"
123171
val SpliceCpfp = "splice-cpfp"
124172
}
173+
174+
/** we can't chart negative amounts in Kamon */
175+
object DiffSigns {
176+
val plus = "plus"
177+
val minus = "minus"
178+
}
125179
}
126180

127181
}

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -783,22 +783,6 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
783783
val localOutputs = session.localOutputs.collect { case o: Output.Local => o }
784784
val remoteOutputs = session.remoteOutputs.collect { case o: Output.Remote => o }
785785

786-
// Global, not "per session". The goal is to measure the total number of inputs/outputs and distribution of amounts across all interactive-tx sessions.
787-
sharedInputs.foreach(i => Monitoring.Metrics.InteractiveTxInputs.withTag(Monitoring.Tags.InputType, "shared").record(i.txOut.amount.toLong))
788-
localInputs.foreach(i => Monitoring.Metrics.InteractiveTxInputs.withTag(Monitoring.Tags.InputType, "local").record(i.txOut.amount.toLong))
789-
remoteInputs.foreach(i => Monitoring.Metrics.InteractiveTxInputs.withTag(Monitoring.Tags.InputType, "remote").record(i.txOut.amount.toLong))
790-
sharedOutputs.foreach(o => Monitoring.Metrics.InteractiveTxOutputs.withTag(Monitoring.Tags.OutputType, "shared").record(o.amount.toLong))
791-
localOutputs.foreach(o => Monitoring.Metrics.InteractiveTxOutputs.withTag(Monitoring.Tags.OutputType, "local").record(o.amount.toLong))
792-
remoteOutputs.foreach(o => Monitoring.Metrics.InteractiveTxOutputs.withTag(Monitoring.Tags.OutputType, "remote").record(o.amount.toLong))
793-
794-
// We measure the number of each input/output type per session.
795-
if (sharedInputs.nonEmpty) Monitoring.Metrics.InteractiveTxInputsPerSession.withTag(Monitoring.Tags.InputType, "shared").record(sharedInputs.size)
796-
if (localInputs.nonEmpty) Monitoring.Metrics.InteractiveTxInputsPerSession.withTag(Monitoring.Tags.InputType, "local").record(localInputs.size)
797-
if (remoteInputs.nonEmpty) Monitoring.Metrics.InteractiveTxInputsPerSession.withTag(Monitoring.Tags.InputType, "remote").record(remoteInputs.size)
798-
if (sharedOutputs.nonEmpty) Monitoring.Metrics.InteractiveTxOutputsPerSession.withTag(Monitoring.Tags.OutputType, "shared").record(sharedOutputs.size)
799-
if (localOutputs.nonEmpty) Monitoring.Metrics.InteractiveTxOutputsPerSession.withTag(Monitoring.Tags.OutputType, "local").record(localOutputs.size)
800-
if (remoteOutputs.nonEmpty) Monitoring.Metrics.InteractiveTxOutputsPerSession.withTag(Monitoring.Tags.OutputType, "remote").record(remoteOutputs.size)
801-
802786
if (sharedOutputs.length > 1) {
803787
log.warn("invalid interactive tx: funding script included multiple times")
804788
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId, "funding script included multiple times"))
@@ -919,6 +903,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
919903
return Left(InvalidCompleteInteractiveTx(fundingParams.channelId, "RBF attempts must double-spend all previous transactions"))
920904
}
921905

906+
Monitoring.Metrics.recordInteractiveTx(fundingParams, sharedTx, liquidityPurchase_opt)
922907
Right(sharedTx)
923908
}
924909

0 commit comments

Comments
 (0)