Skip to content

Commit aeae8cc

Browse files
committed
More metrics and test
1 parent a3fb1ac commit aeae8cc

5 files changed

Lines changed: 196 additions & 3 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2020
import fr.acinq.eclair.MilliSatoshi
2121
import fr.acinq.eclair.channel.CMD_FAIL_HTLC
2222
import kamon.Kamon
23+
import kamon.metric.Histogram
2324

2425
object Monitoring {
2526

@@ -67,6 +68,10 @@ object Monitoring {
6768
PaymentNodeOutAmount.withoutTags().record(bucket, amount.truncateToSatoshi.toLong)
6869
PaymentNodeOut.withoutTags().record(bucket)
6970
}
71+
72+
private val RelayConfidence = Kamon.histogram("payment.relay.confidence", "Confidence (in percent) that the relayed HTLC will be fulfilled")
73+
def relaySettleFulfill(confidence: Double) = RelayConfidence.withTag("status", "fulfill").record((confidence * 100).toLong)
74+
def relaySettleFail(confidence: Double) = RelayConfidence.withTag("status", "fail").record((confidence * 100).toLong)
7075
}
7176

7277
object Tags {

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,20 +163,22 @@ class ChannelRelay private(nodeParams: NodeParams,
163163

164164
case WrappedAddResponse(_: RES_SUCCESS[_]) =>
165165
context.log.debug("sent htlc to the downstream channel")
166-
waitForAddSettled()
166+
waitForAddSettled(confidence)
167167
}
168168

169-
def waitForAddSettled(): Behavior[Command] =
169+
def waitForAddSettled(confidence: Double): Behavior[Command] =
170170
Behaviors.receiveMessagePartial {
171171
case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, htlc, fulfill: HtlcResult.Fulfill)) =>
172172
context.log.debug("relaying fulfill to upstream")
173+
Metrics.relaySettleFulfill(confidence)
173174
val cmd = CMD_FULFILL_HTLC(o.originHtlcId, fulfill.paymentPreimage, commit = true)
174175
context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(o.amountIn, o.amountOut, htlc.paymentHash, o.originChannelId, htlc.channelId, startedAt, TimestampMilli.now()))
175176
recordRelayDuration(isSuccess = true)
176177
safeSendAndStop(o.originChannelId, cmd)
177178

178179
case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, _, fail: HtlcResult.Fail)) =>
179180
context.log.debug("relaying fail to upstream")
181+
Metrics.relaySettleFail(confidence)
180182
Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel)
181183
val cmd = translateRelayFailure(o.originHtlcId, fail)
182184
recordRelayDuration(isSuccess = false)

eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ object ReputationRecorder {
7171
r.updated((originNode, isEndorsed), updatedReputation)
7272
}
7373
ReputationRecorder(reputationConfig, updatedReputations)
74-
7574
}
7675
}
7776
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2023 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+
17+
package fr.acinq.eclair.reputation
18+
19+
import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
20+
import akka.actor.typed.ActorRef
21+
import com.typesafe.config.ConfigFactory
22+
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
23+
import fr.acinq.eclair.reputation.Reputation.ReputationConfig
24+
import fr.acinq.eclair.reputation.ReputationRecorder._
25+
import fr.acinq.eclair.{MilliSatoshiLong, randomKey}
26+
import org.scalatest.Outcome
27+
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
28+
29+
import java.util.UUID
30+
import scala.concurrent.duration.DurationInt
31+
32+
class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike {
33+
val (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6, uuid7, uuid8) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
34+
val originNode: PublicKey = randomKey().publicKey
35+
36+
case class FixtureParam(config: ReputationConfig, reputationRecorder: ActorRef[Command], replyTo: TestProbe[Confidence])
37+
38+
override def withFixture(test: OneArgTest): Outcome = {
39+
val config = ReputationConfig(1000000000 msat, 10 seconds)
40+
val replyTo = TestProbe[Confidence]("confidence")
41+
val reputationRecorder = testKit.spawn(ReputationRecorder(config, Map.empty))
42+
withFixture(test.toNoArgTest(FixtureParam(config, reputationRecorder.ref, replyTo)))
43+
}
44+
45+
test("standard") { f =>
46+
import f._
47+
48+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid1, 1100 msat)
49+
assert(replyTo.expectMessageType[Confidence].value == 0)
50+
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid1, isSuccess = true)
51+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid2, 1100 msat)
52+
assert(replyTo.expectMessageType[Confidence].value == 0.5)
53+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid3, 1100 msat)
54+
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
55+
reputationRecorder ! CancelRelay(originNode, isEndorsed = true, uuid3)
56+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid4, 1100 msat)
57+
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
58+
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid4, isSuccess = true)
59+
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid2, isSuccess = false)
60+
// Not endorsed
61+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = false, uuid5, 1100 msat)
62+
assert(replyTo.expectMessageType[Confidence].value == 0)
63+
// Different origin node
64+
reputationRecorder ! GetConfidence(replyTo.ref, randomKey().publicKey, isEndorsed = true, uuid6, 1100 msat)
65+
assert(replyTo.expectMessageType[Confidence].value == 0)
66+
// Very large HTLC
67+
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid5, 10000000 msat)
68+
assert(replyTo.expectMessageType[Confidence].value === 0.0 +- 0.001)
69+
}
70+
71+
test("trampoline") { f =>
72+
import f._
73+
74+
val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey)
75+
76+
reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (b, true) -> 2000.msat, (c, false) -> 3000.msat), uuid1)
77+
assert(replyTo.expectMessageType[Confidence].value == 0)
78+
reputationRecorder ! RecordTrampolineSuccess(Map((a, true) -> 500.msat, (b, true) -> 1000.msat, (c, false) -> 1500.msat), uuid1)
79+
reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (c, false) -> 1000.msat), uuid2)
80+
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
81+
reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3)
82+
assert(replyTo.expectMessageType[Confidence].value == 0)
83+
reputationRecorder ! RecordTrampolineFailure(Set((a, true), (c, false)), uuid2)
84+
reputationRecorder ! RecordTrampolineSuccess(Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3)
85+
86+
reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = true, uuid4, 1000 msat)
87+
assert(replyTo.expectMessageType[Confidence].value === 0.2 +- 0.001)
88+
reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = false, uuid5, 1000 msat)
89+
assert(replyTo.expectMessageType[Confidence].value === 0.5 +- 0.001)
90+
reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = true, uuid6, 1000 msat)
91+
assert(replyTo.expectMessageType[Confidence].value === 0.75 +- 0.001)
92+
reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = false, uuid7, 1000 msat)
93+
assert(replyTo.expectMessageType[Confidence].value == 0.0)
94+
reputationRecorder ! GetConfidence(replyTo.ref, c, isEndorsed = false, uuid8, 1000 msat)
95+
assert(replyTo.expectMessageType[Confidence].value === (3.0 / 7) +- 0.001)
96+
}
97+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2023 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+
17+
package fr.acinq.eclair.reputation
18+
19+
import fr.acinq.eclair.{MilliSatoshiLong, TimestampMilli}
20+
import fr.acinq.eclair.reputation.Reputation.ReputationConfig
21+
import org.scalatest.funsuite.AnyFunSuite
22+
import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper
23+
24+
import java.util.UUID
25+
import scala.concurrent.duration.DurationInt
26+
27+
class ReputationSpec extends AnyFunSuite {
28+
val (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6, uuid7) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
29+
30+
test("basic") {
31+
var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second))
32+
r = r.attempt(uuid1, 10000 msat)
33+
assert(r.confidence() == 0)
34+
r = r.record(uuid1, isSuccess = true)
35+
r = r.attempt(uuid2, 10000 msat)
36+
assert(r.confidence() == 0.5)
37+
r = r.attempt(uuid3, 10000 msat)
38+
assert(r.confidence() === 0.333 +- 0.001)
39+
r = r.record(uuid2, isSuccess = true)
40+
r = r.record(uuid3, isSuccess = true)
41+
r = r.attempt(uuid4, 1 msat)
42+
assert(r.confidence() === 1.0 +- 0.001)
43+
r = r.attempt(uuid5, 90000 msat)
44+
assert(r.confidence() === 0.25 +- 0.001)
45+
r = r.attempt(uuid6, 10000 msat)
46+
assert(r.confidence() === (3.0 / 13) +- 0.001)
47+
r = r.cancel(uuid5)
48+
assert(r.confidence() === 0.75 +- 0.001)
49+
r = r.record(uuid6, isSuccess = false)
50+
assert(r.confidence() === 0.75 +- 0.001)
51+
r = r.attempt(uuid7, 10000 msat)
52+
assert(r.confidence() === 0.6 +- 0.001)
53+
}
54+
55+
test("long HTLC") {
56+
var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second))
57+
r = r.attempt(uuid1, 100000 msat)
58+
assert(r.confidence() == 0)
59+
r = r.record(uuid1, isSuccess = true)
60+
assert(r.confidence() == 1)
61+
r = r.attempt(uuid2, 1000 msat, TimestampMilli(0))
62+
assert(r.confidence(TimestampMilli(0)) === 0.99 +- 0.001)
63+
assert(r.confidence(TimestampMilli(0) + 100.seconds) == 0.5)
64+
r = r.record(uuid2, isSuccess = false, now = TimestampMilli(0) + 100.seconds)
65+
assert(r.confidence() == 0.5)
66+
}
67+
68+
test("max weight") {
69+
var r = Reputation.init(ReputationConfig(1000000 msat, 1 second))
70+
// build perfect reputation
71+
for(i <- 1 to 100){
72+
val uuid = UUID.randomUUID()
73+
r = r.attempt(uuid, 100000 msat)
74+
r = r.record(uuid, isSuccess = true)
75+
}
76+
assert(r.confidence() == 1)
77+
r = r.attempt(uuid1, 100000 msat)
78+
assert(r.confidence() === 0.91 +- 0.01)
79+
r = r.record(uuid1, isSuccess = false)
80+
assert(r.confidence() === 0.91 +- 0.01)
81+
r = r.attempt(uuid2, 100000 msat)
82+
assert(r.confidence() === 0.83 +- 0.01)
83+
r = r.record(uuid2, isSuccess = false)
84+
assert(r.confidence() === 0.83 +- 0.01)
85+
r = r.attempt(uuid3, 100000 msat)
86+
assert(r.confidence() === 0.75 +- 0.01)
87+
r = r.record(uuid3, isSuccess = false)
88+
assert(r.confidence() === 0.75 +- 0.01)
89+
}
90+
}

0 commit comments

Comments
 (0)