Skip to content

Commit

Permalink
More metrics and test
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Jul 24, 2023
1 parent d70ca33 commit 525f547
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.channel.CMD_FAIL_HTLC
import kamon.Kamon
import kamon.metric.Histogram

object Monitoring {

Expand Down Expand Up @@ -67,6 +68,10 @@ object Monitoring {
PaymentNodeOutAmount.withoutTags().record(bucket, amount.truncateToSatoshi.toLong)
PaymentNodeOut.withoutTags().record(bucket)
}

private val RelayConfidence = Kamon.histogram("payment.relay.confidence", "Confidence (in percent) that the relayed HTLC will be fulfilled")
def relaySettleFulfill(confidence: Double) = RelayConfidence.withTag("status", "fulfill").record((confidence * 100).toLong)
def relaySettleFail(confidence: Double) = RelayConfidence.withTag("status", "fail").record((confidence * 100).toLong)
}

object Tags {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,22 @@ class ChannelRelay private(nodeParams: NodeParams,

case WrappedAddResponse(_: RES_SUCCESS[_]) =>
context.log.debug("sent htlc to the downstream channel")
waitForAddSettled()
waitForAddSettled(confidence)
}

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

case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, _, fail: HtlcResult.Fail)) =>
context.log.debug("relaying fail to upstream")
Metrics.relaySettleFail(confidence)
Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel)
val cmd = translateRelayFailure(o.originHtlcId, fail)
recordRelayDuration(isSuccess = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ object ReputationRecorder {
r.updated((originNode, isEndorsed), updatedReputation)
}
ReputationRecorder(reputationConfig, updatedReputations)

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.reputation

import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
import akka.actor.typed.ActorRef
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.reputation.Reputation.ReputationConfig
import fr.acinq.eclair.reputation.ReputationRecorder._
import fr.acinq.eclair.{MilliSatoshiLong, randomKey}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike

import java.util.UUID
import scala.concurrent.duration.DurationInt

class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike {
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())
val originNode: PublicKey = randomKey().publicKey

case class FixtureParam(config: ReputationConfig, reputationRecorder: ActorRef[Command], replyTo: TestProbe[Confidence])

override def withFixture(test: OneArgTest): Outcome = {
val config = ReputationConfig(1000000000 msat, 10 seconds)
val replyTo = TestProbe[Confidence]("confidence")
val reputationRecorder = testKit.spawn(ReputationRecorder(config, Map.empty))
withFixture(test.toNoArgTest(FixtureParam(config, reputationRecorder.ref, replyTo)))
}

test("standard") { f =>
import f._

reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid1, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value == 0)
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid1, isSuccess = true)
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid2, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value == 0.5)
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid3, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
reputationRecorder ! CancelRelay(originNode, isEndorsed = true, uuid3)
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid4, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid4, isSuccess = true)
reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid2, isSuccess = false)
// Not endorsed
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = false, uuid5, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value == 0)
// Different origin node
reputationRecorder ! GetConfidence(replyTo.ref, randomKey().publicKey, isEndorsed = true, uuid6, 1100 msat)
assert(replyTo.expectMessageType[Confidence].value == 0)
// Very large HTLC
reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid5, 10000000 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.0 +- 0.001)
}

test("trampoline") { f =>
import f._

val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey)

reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (b, true) -> 2000.msat, (c, false) -> 3000.msat), uuid1)
assert(replyTo.expectMessageType[Confidence].value == 0)
reputationRecorder ! RecordTrampolineSuccess(Map((a, true) -> 500.msat, (b, true) -> 1000.msat, (c, false) -> 1500.msat), uuid1)
reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (c, false) -> 1000.msat), uuid2)
assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001)
reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3)
assert(replyTo.expectMessageType[Confidence].value == 0)
reputationRecorder ! RecordTrampolineFailure(Set((a, true), (c, false)), uuid2)
reputationRecorder ! RecordTrampolineSuccess(Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3)

reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = true, uuid4, 1000 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.2 +- 0.001)
reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = false, uuid5, 1000 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.5 +- 0.001)
reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = true, uuid6, 1000 msat)
assert(replyTo.expectMessageType[Confidence].value === 0.75 +- 0.001)
reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = false, uuid7, 1000 msat)
assert(replyTo.expectMessageType[Confidence].value == 0.0)
reputationRecorder ! GetConfidence(replyTo.ref, c, isEndorsed = false, uuid8, 1000 msat)
assert(replyTo.expectMessageType[Confidence].value === (3.0 / 7) +- 0.001)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.reputation

import fr.acinq.eclair.{MilliSatoshiLong, TimestampMilli}
import fr.acinq.eclair.reputation.Reputation.ReputationConfig
import org.scalatest.funsuite.AnyFunSuite
import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper

import java.util.UUID
import scala.concurrent.duration.DurationInt

class ReputationSpec extends AnyFunSuite {
val (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6, uuid7) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())

test("basic") {
var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second))
r = r.attempt(uuid1, 10000 msat)
assert(r.confidence() == 0)
r = r.record(uuid1, isSuccess = true)
r = r.attempt(uuid2, 10000 msat)
assert(r.confidence() == 0.5)
r = r.attempt(uuid3, 10000 msat)
assert(r.confidence() === 0.333 +- 0.001)
r = r.record(uuid2, isSuccess = true)
r = r.record(uuid3, isSuccess = true)
r = r.attempt(uuid4, 1 msat)
assert(r.confidence() === 1.0 +- 0.001)
r = r.attempt(uuid5, 90000 msat)
assert(r.confidence() === 0.25 +- 0.001)
r = r.attempt(uuid6, 10000 msat)
assert(r.confidence() === (3.0 / 13) +- 0.001)
r = r.cancel(uuid5)
assert(r.confidence() === 0.75 +- 0.001)
r = r.record(uuid6, isSuccess = false)
assert(r.confidence() === 0.75 +- 0.001)
r = r.attempt(uuid7, 10000 msat)
assert(r.confidence() === 0.6 +- 0.001)
}

test("long HTLC") {
var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second))
r = r.attempt(uuid1, 100000 msat)
assert(r.confidence() == 0)
r = r.record(uuid1, isSuccess = true)
assert(r.confidence() == 1)
r = r.attempt(uuid2, 1000 msat, TimestampMilli(0))
assert(r.confidence(TimestampMilli(0)) === 0.99 +- 0.001)
assert(r.confidence(TimestampMilli(0) + 100.seconds) == 0.5)
r = r.record(uuid2, isSuccess = false, now = TimestampMilli(0) + 100.seconds)
assert(r.confidence() == 0.5)
}

test("max weight") {
var r = Reputation.init(ReputationConfig(1000000 msat, 1 second))
// build perfect reputation
for(i <- 1 to 100){
val uuid = UUID.randomUUID()
r = r.attempt(uuid, 100000 msat)
r = r.record(uuid, isSuccess = true)
}
assert(r.confidence() == 1)
r = r.attempt(uuid1, 100000 msat)
assert(r.confidence() === 0.91 +- 0.01)
r = r.record(uuid1, isSuccess = false)
assert(r.confidence() === 0.91 +- 0.01)
r = r.attempt(uuid2, 100000 msat)
assert(r.confidence() === 0.83 +- 0.01)
r = r.record(uuid2, isSuccess = false)
assert(r.confidence() === 0.83 +- 0.01)
r = r.attempt(uuid3, 100000 msat)
assert(r.confidence() === 0.75 +- 0.01)
r = r.record(uuid3, isSuccess = false)
assert(r.confidence() === 0.75 +- 0.01)
}
}

0 comments on commit 525f547

Please sign in to comment.