From c29aa18c40d5b9fc18da0f0830e2675fb2edcca5 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 23 Jul 2025 11:21:49 +0200 Subject: [PATCH] Use actual CLTV delta for reputation --- docs/release-notes/eclair-vnext.md | 7 +- eclair-core/src/main/resources/reference.conf | 9 +- .../scala/fr/acinq/eclair/NodeParams.scala | 1 - .../fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../eclair/payment/relay/ChannelRelay.scala | 2 +- .../payment/send/PaymentLifecycle.scala | 4 +- .../acinq/eclair/reputation/Reputation.scala | 45 +++--- .../reputation/ReputationRecorder.scala | 26 ++-- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../reputation/ReputationRecorderSpec.scala | 70 +++++----- .../eclair/reputation/ReputationSpec.scala | 132 ++++++++++-------- 11 files changed, 161 insertions(+), 141 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index a3bf9d01cf..6d3f046696 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -43,12 +43,9 @@ eclair.relay.peer-reputation { // value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md, enabled = true // Reputation decays with the following half life to emphasize recent behavior. - half-life = 15 days + half-life = 30 days // Payments that stay pending for longer than this get penalized - max-relay-duration = 12 seconds - // Pending payments are counted as failed, and because they could potentially stay pending for a very long time, - // the following multiplier is applied. - pending-multiplier = 200 // A pending payment counts as two hundred failed ones. + max-relay-duration = 5 minutes } ``` diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 2bce28be64..2c68f5dcba 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -259,14 +259,9 @@ eclair { // value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md, enabled = true // Reputation decays with the following half life to emphasize recent behavior. - half-life = 15 days + half-life = 30 days // Payments that stay pending for longer than this get penalized. - max-relay-duration = 12 seconds - // Pending payments are counted as failed, and because they could potentially stay pending for a very long time, - // the following multiplier is applied. We want it to be as close as possible to the true cost of a worst case - // HTLC (max-cltv-delta / max-relay-duration, around 100000 with default parameters) while still being comparable - // to the number of HTLCs received per peer during twice the half life. - pending-multiplier = 200 // A pending payment counts as two hundred failed ones. + max-relay-duration = 5 minutes } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 9ca454c59b..aab99e4aa9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -647,7 +647,6 @@ object NodeParams extends Logging { enabled = config.getBoolean("relay.peer-reputation.enabled"), halfLife = FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS), maxRelayDuration = FiniteDuration(config.getDuration("relay.peer-reputation.max-relay-duration").getSeconds, TimeUnit.SECONDS), - pendingMultiplier = config.getDouble("relay.peer-reputation.pending-multiplier"), ), ), db = database, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 86ab8a8cdc..8c3b574dfb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -517,7 +517,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) val relayFee = nodeFee(d.channelUpdate.relayFees, add.amountMsat) context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, c.origin.upstream, relayFee)) - log.info("OutgoingHtlcAdded: channelId={}, id={}, endorsement={}, remoteNodeId={}, upstream={}, fee={}", Array(add.channelId.toHex, add.id, add.endorsement, remoteNodeId.toHex, c.origin.upstream.toString, relayFee)) + log.info("OutgoingHtlcAdded: channelId={}, id={}, endorsement={}, remoteNodeId={}, upstream={}, fee={}, now={}, blockHeight={}, expiry={}", Array(add.channelId.toHex, add.id, add.endorsement, remoteNodeId.toHex, c.origin.upstream.toString, relayFee, TimestampMilli.now().toLong, nodeParams.currentBlockHeight.toLong, add.cltvExpiry)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index b69d6d6587..d4c1ae46c3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -79,7 +79,7 @@ object ChannelRelay { val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), r.receivedAt, originNode, incomingChannelOccupancy) reputationRecorder_opt match { case Some(reputationRecorder) => - reputationRecorder ! GetConfidence(context.messageAdapter(WrappedReputationScore(_)), upstream, channels.values.headOption.map(_.nextNodeId), r.relayFeeMsat) + reputationRecorder ! GetConfidence(context.messageAdapter(WrappedReputationScore(_)), upstream, channels.values.headOption.map(_.nextNodeId), r.relayFeeMsat, nodeParams.currentBlockHeight, r.outgoingCltv) case None => context.self ! WrappedReputationScore(Reputation.Score.fromEndorsement(r.add.endorsement)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index d673248a59..aa564e0798 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -77,7 +77,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(RouteResponse(route +: _), WaitingForRoute(request, failures, ignore)) => log.info(s"route found: attempt=${failures.size + 1}/${request.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") reputationRecorder_opt match { - case Some(reputationRecorder) => reputationRecorder ! GetConfidence(self, cfg.upstream, Some(route.hops.head.nextNodeId), route.hops.head.fee(request.amount)) + case Some(reputationRecorder) => + val cltvExpiry = route.fullRoute.map(_.cltvExpiryDelta).foldLeft(request.recipient.expiry)(_ + _) + reputationRecorder ! GetConfidence(self, cfg.upstream, Some(route.hops.head.nextNodeId), route.hops.head.fee(request.amount), nodeParams.currentBlockHeight, cltvExpiry) case None => val endorsement = cfg.upstream match { case Hot.Channel(add, _, _, _) => add.endorsement diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala index aee51b7b59..adf9ffbaed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala @@ -20,9 +20,9 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.{ChannelJammingException, ChannelParams, Commitments, IncomingConfidenceTooLow, OutgoingConfidenceTooLow, TooManySmallHtlcs} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{MilliSatoshi, TimestampMilli} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, TimestampMilli} -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration.{DurationInt, FiniteDuration} /** * Reputation score per endorsement level. @@ -34,15 +34,21 @@ import scala.concurrent.duration.FiniteDuration case class PastScore(weight: Double, score: Double, lastSettlementAt: TimestampMilli) /** We're relaying that HTLC and are waiting for it to settle. */ -case class PendingHtlc(fee: MilliSatoshi, endorsement: Int, startedAt: TimestampMilli) { - def weight(now: TimestampMilli, minDuration: FiniteDuration, multiplier: Double): Double = { - val duration = now - startedAt - fee.toLong.toDouble * (duration / minDuration).max(multiplier) +case class PendingHtlc(fee: MilliSatoshi, endorsement: Int, startedAt: TimestampMilli, expiry: CltvExpiry) { + def weight(now: TimestampMilli, minDuration: FiniteDuration, currentBlockHeight: BlockHeight): Double = { + val alreadyPending = now - startedAt + val untilExpiry = (expiry.toLong - currentBlockHeight.toLong) * 10.minutes + val duration = alreadyPending + untilExpiry + fee.toLong.toDouble * (duration / minDuration) } } case class HtlcId(channelId: ByteVector32, id: Long) +case object HtlcId { + def apply(add: UpdateAddHtlc): HtlcId = HtlcId(add.channelId, add.id) +} + /** * Local reputation for a given node. * @@ -50,15 +56,14 @@ case class HtlcId(channelId: ByteVector32, id: Long) * @param pending Set of pending HTLCs. * @param halfLife Half life for the exponential moving average. * @param maxRelayDuration Duration after which HTLCs are penalized for staying pending too long. - * @param pendingMultiplier How much to penalize pending HTLCs. */ -case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, PendingHtlc], halfLife: FiniteDuration, maxRelayDuration: FiniteDuration, pendingMultiplier: Double) { +case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, PendingHtlc], halfLife: FiniteDuration, maxRelayDuration: FiniteDuration) { private def decay(now: TimestampMilli, lastSettlementAt: TimestampMilli): Double = scala.math.pow(0.5, (now - lastSettlementAt) / halfLife) /** * Estimate the confidence that a payment will succeed. */ - def getConfidence(fee: MilliSatoshi, endorsement: Int, now: TimestampMilli = TimestampMilli.now()): Double = { + def getConfidence(fee: MilliSatoshi, endorsement: Int, currentBlockHeight: BlockHeight, expiry: CltvExpiry, now: TimestampMilli = TimestampMilli.now()): Double = { val weights = Array.fill(Reputation.endorsementLevels)(0.0) val scores = Array.fill(Reputation.endorsementLevels)(0.0) for (e <- 0 until Reputation.endorsementLevels) { @@ -67,9 +72,9 @@ case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, Pend scores(e) += d * pastScores(e).score } for (p <- pending.values) { - weights(p.endorsement) += p.weight(now, maxRelayDuration, pendingMultiplier) + weights(p.endorsement) += p.weight(now, maxRelayDuration, currentBlockHeight) } - weights(endorsement) += fee.toLong.toDouble * pendingMultiplier + weights(endorsement) += PendingHtlc(fee, endorsement, now, expiry).weight(now, maxRelayDuration, currentBlockHeight) /* Higher endorsement buckets may have fewer payments which makes the weight of pending payments disproportionately important. To counter this effect, we try adding payments from the lower buckets to see if it gives us a higher @@ -91,8 +96,8 @@ case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, Pend /** * Register a pending relay. */ - def addPendingHtlc(htlcId: HtlcId, fee: MilliSatoshi, endorsement: Int, now: TimestampMilli = TimestampMilli.now()): Reputation = - copy(pending = pending + (htlcId -> PendingHtlc(fee, endorsement, now))) + def addPendingHtlc(add: UpdateAddHtlc, fee: MilliSatoshi, endorsement: Int, now: TimestampMilli = TimestampMilli.now()): Reputation = + copy(pending = pending + (HtlcId(add) -> PendingHtlc(fee, endorsement, now, add.cltvExpiry))) /** * When a HTLC is settled, we record whether it succeeded and how long it took. @@ -100,8 +105,14 @@ case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, Pend def settlePendingHtlc(htlcId: HtlcId, isSuccess: Boolean, now: TimestampMilli = TimestampMilli.now()): Reputation = { val newScores = pending.get(htlcId).map(p => { val d = decay(now, pastScores(p.endorsement).lastSettlementAt) - val newWeight = d * pastScores(p.endorsement).weight + p.weight(now, maxRelayDuration, if (isSuccess) 1.0 else 0.0) - val newScore = d * pastScores(p.endorsement).score + (if (isSuccess) p.fee.toLong.toDouble else 0) + val duration = now - p.startedAt + val (weight, score) = if (isSuccess) { + (p.fee.toLong.toDouble * (duration / maxRelayDuration).max(1.0), p.fee.toLong.toDouble) + } else { + (p.fee.toLong.toDouble * (duration / maxRelayDuration), 0.0) + } + val newWeight = d * pastScores(p.endorsement).weight + weight + val newScore = d * pastScores(p.endorsement).score + score pastScores + (p.endorsement -> PastScore(newWeight, newScore, now)) }).getOrElse(pastScores) copy(pending = pending - htlcId, pastScores = newScores) @@ -112,9 +123,9 @@ object Reputation { val endorsementLevels = 8 val maxEndorsement = endorsementLevels - 1 - case class Config(enabled: Boolean, halfLife: FiniteDuration, maxRelayDuration: FiniteDuration, pendingMultiplier: Double) + case class Config(enabled: Boolean, halfLife: FiniteDuration, maxRelayDuration: FiniteDuration) - def init(config: Config): Reputation = Reputation(Map.empty.withDefaultValue(PastScore(0.0, 0.0, TimestampMilli.min)), Map.empty, config.halfLife, config.maxRelayDuration, config.pendingMultiplier) + def init(config: Config): Reputation = Reputation(Map.empty.withDefaultValue(PastScore(0.0, 0.0, TimestampMilli.min)), Map.empty, config.halfLife, config.maxRelayDuration) /** * @param incomingConfidence Confidence that the outgoing HTLC will succeed given the reputation of the incoming peer diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala index cecc4482c1..f480a921b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala @@ -20,7 +20,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi} import fr.acinq.eclair.channel.Upstream.Hot import fr.acinq.eclair.channel.{OutgoingHtlcAdded, OutgoingHtlcFailed, OutgoingHtlcFulfilled, OutgoingHtlcSettled, Upstream} import fr.acinq.eclair.reputation.ReputationRecorder._ @@ -31,7 +31,7 @@ import scala.collection.mutable object ReputationRecorder { // @formatter:off sealed trait Command - case class GetConfidence(replyTo: ActorRef[Reputation.Score], upstream: Upstream.Hot, downstream_opt: Option[PublicKey], fee: MilliSatoshi) extends Command + case class GetConfidence(replyTo: ActorRef[Reputation.Score], upstream: Upstream.Hot, downstream_opt: Option[PublicKey], fee: MilliSatoshi, currentBlockHeight: BlockHeight, expiry: CltvExpiry) extends Command private case class WrappedOutgoingHtlcAdded(added: OutgoingHtlcAdded) extends Command private case class WrappedOutgoingHtlcSettled(settled: OutgoingHtlcSettled) extends Command // @formatter:on @@ -60,17 +60,17 @@ class ReputationRecorder(config: Reputation.Config) { def run(): Behavior[Command] = Behaviors.receiveMessage { - case GetConfidence(replyTo, _: Upstream.Local, _, _) => + case GetConfidence(replyTo, _: Upstream.Local, _, _, _, _) => replyTo ! Reputation.Score.max Behaviors.same - case GetConfidence(replyTo, upstream: Upstream.Hot.Channel, downstream_opt, fee) => - val incomingConfidence = incomingReputations.get(upstream.receivedFrom).map(_.getConfidence(fee, upstream.add.endorsement)).getOrElse(0.0) - val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(fee, Reputation.toEndorsement(incomingConfidence))).getOrElse(0.0) + case GetConfidence(replyTo, upstream: Upstream.Hot.Channel, downstream_opt, fee, currentBlockHeight, expiry) => + val incomingConfidence = incomingReputations.get(upstream.receivedFrom).map(_.getConfidence(fee, upstream.add.endorsement, currentBlockHeight, expiry)).getOrElse(0.0) + val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(fee, Reputation.toEndorsement(incomingConfidence), currentBlockHeight, expiry)).getOrElse(0.0) replyTo ! Reputation.Score(incomingConfidence, outgoingConfidence) Behaviors.same - case GetConfidence(replyTo, upstream: Upstream.Hot.Trampoline, downstream_opt, totalFee) => + case GetConfidence(replyTo, upstream: Upstream.Hot.Trampoline, downstream_opt, totalFee, currentBlockHeight, expiry) => val incomingConfidence = upstream.received .groupMapReduce(_.receivedFrom)(r => (r.add.amountMsat, r.add.endorsement)) { @@ -79,29 +79,29 @@ class ReputationRecorder(config: Reputation.Config) { .map { case (nodeId, (amount, endorsement)) => val fee = amount * totalFee.toLong / upstream.amountIn.toLong - incomingReputations.get(nodeId).map(_.getConfidence(fee, endorsement)).getOrElse(0.0) + incomingReputations.get(nodeId).map(_.getConfidence(fee, endorsement, currentBlockHeight, expiry)).getOrElse(0.0) } .min - val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(totalFee, Reputation.toEndorsement(incomingConfidence))).getOrElse(0.0) + val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(totalFee, Reputation.toEndorsement(incomingConfidence), currentBlockHeight, expiry)).getOrElse(0.0) replyTo ! Reputation.Score(incomingConfidence, outgoingConfidence) Behaviors.same case WrappedOutgoingHtlcAdded(OutgoingHtlcAdded(add, remoteNodeId, upstream, fee)) => - val htlcId = HtlcId(add.channelId, add.id) + val htlcId = HtlcId(add) upstream match { case channel: Hot.Channel => - incomingReputations(channel.receivedFrom) = incomingReputations(channel.receivedFrom).addPendingHtlc(htlcId, fee, channel.add.endorsement) + incomingReputations(channel.receivedFrom) = incomingReputations(channel.receivedFrom).addPendingHtlc(add, fee, channel.add.endorsement) case trampoline: Hot.Trampoline => trampoline.received .groupMapReduce(_.receivedFrom)(r => (r.add.amountMsat, r.add.endorsement)) { case ((amount1, endorsement1), (amount2, endorsement2)) => (amount1 + amount2, endorsement1 min endorsement2) } .foreach { case (nodeId, (amount, endorsement)) => - incomingReputations(nodeId) = incomingReputations(nodeId).addPendingHtlc(htlcId, fee * amount.toLong / trampoline.amountIn.toLong, endorsement) + incomingReputations(nodeId) = incomingReputations(nodeId).addPendingHtlc(add, fee * amount.toLong / trampoline.amountIn.toLong, endorsement) } case _: Upstream.Local => () } - outgoingReputations(remoteNodeId) = outgoingReputations(remoteNodeId).addPendingHtlc(htlcId, fee, add.endorsement) + outgoingReputations(remoteNodeId) = outgoingReputations(remoteNodeId).addPendingHtlc(add, fee, add.endorsement) pending(htlcId) = PendingHtlc(add, upstream, remoteNodeId) Behaviors.same diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index f01d37998b..f6ea6efc6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -180,7 +180,7 @@ object TestConstants { feeProportionalMillionths = 30), enforcementDelay = 10 minutes, asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), - peerReputationConfig = Reputation.Config(enabled = true, 1 day, 10 seconds, 100), + peerReputationConfig = Reputation.Config(enabled = true, 1 day, 10 minutes), ), db = TestDatabases.inMemoryDb(), autoReconnect = false, @@ -372,7 +372,7 @@ object TestConstants { feeProportionalMillionths = 30), enforcementDelay = 10 minutes, asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), - peerReputationConfig = Reputation.Config(enabled = true, 2 day, 20 seconds, 200), + peerReputationConfig = Reputation.Config(enabled = true, 2 day, 20 minutes), ), db = TestDatabases.inMemoryDb(), autoReconnect = false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala index 5e6ba0a9ed..61eeb64d0a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{OutgoingHtlcAdded, OutgoingHtlcFailed, OutgoingHtlcFulfilled, Upstream} import fr.acinq.eclair.reputation.ReputationRecorder._ import fr.acinq.eclair.wire.protocol.{TlvStream, UpdateAddHtlc, UpdateAddHtlcTlv, UpdateFailHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TimestampMilli, randomBytes, randomBytes32, randomKey, randomLong} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, TimestampMilli, randomBytes, randomBytes32, randomKey, randomLong} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -37,7 +37,7 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa case class FixtureParam(config: Reputation.Config, reputationRecorder: ActorRef[Command], replyTo: TestProbe[Reputation.Score]) override def withFixture(test: OneArgTest): Outcome = { - val config = Reputation.Config(enabled = true, 1 day, 10 seconds, 2) + val config = Reputation.Config(enabled = true, 1 day, 10 minutes) val replyTo = TestProbe[Reputation.Score]("confidence") val reputationRecorder = testKit.spawn(ReputationRecorder(config)) withFixture(test.toNoArgTest(FixtureParam(config, reputationRecorder.ref, replyTo))) @@ -46,8 +46,8 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa def makeChannelUpstream(nodeId: PublicKey, endorsement: Int, amount: MilliSatoshi = 1000000 msat): Upstream.Hot.Channel = Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), randomLong(), amount, randomBytes32(), CltvExpiry(1234), null, TlvStream(UpdateAddHtlcTlv.Endorsement(endorsement))), TimestampMilli.now(), nodeId, 0.25) - def makeOutgoingHtlcAdded(upstream: Upstream.Hot, downstream: PublicKey, fee: MilliSatoshi): OutgoingHtlcAdded = - OutgoingHtlcAdded(UpdateAddHtlc(randomBytes32(), randomLong(), 100000 msat, randomBytes32(), CltvExpiry(456), null, TlvStream.empty), downstream, upstream, fee) + def makeOutgoingHtlcAdded(upstream: Upstream.Hot, downstream: PublicKey, fee: MilliSatoshi, expiry: CltvExpiry): OutgoingHtlcAdded = + OutgoingHtlcAdded(UpdateAddHtlc(randomBytes32(), randomLong(), 100000 msat, randomBytes32(), expiry, null, TlvStream.empty), downstream, upstream, fee) def makeOutgoingHtlcFulfilled(add: UpdateAddHtlc): OutgoingHtlcFulfilled = OutgoingHtlcFulfilled(UpdateFulfillHtlc(add.channelId, add.id, randomBytes32(), TlvStream.empty)) @@ -61,48 +61,48 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa val (nextA, nextB) = (randomKey().publicKey, randomKey().publicKey) val upstream1 = makeChannelUpstream(originNode, 7) - reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(nextA), 2000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(nextA), 2000 msat, BlockHeight(0), CltvExpiry(2)) replyTo.expectMessage(Reputation.Score(0.0, 0.0)) - val added1 = makeOutgoingHtlcAdded(upstream1, nextA, 2000 msat) + val added1 = makeOutgoingHtlcAdded(upstream1, nextA, 2000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added1) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added1.add)) val upstream2 = makeChannelUpstream(originNode, 7) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(nextB), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(nextB), 1000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (2.0 / 4) +- 0.001 && score.outgoingConfidence == 0.0 }, max = 60 seconds) - val added2 = makeOutgoingHtlcAdded(upstream2, nextB, 1000 msat) + val added2 = makeOutgoingHtlcAdded(upstream2, nextB, 1000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added2) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 3000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 3000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (2.0 / 10) +- 0.001 && score.outgoingConfidence === (2.0 / 8) +- 0.001 }, max = 60 seconds) val upstream3 = makeChannelUpstream(originNode, 7) - reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(nextB), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(nextB), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (2.0 / 6) +- 0.001 && score.outgoingConfidence == 0.0 }) - val added3 = makeOutgoingHtlcAdded(upstream3, nextB, 1000 msat) + val added3 = makeOutgoingHtlcAdded(upstream3, nextB, 1000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added3) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added3.add)) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFailed(added2.add)) // Not endorsed awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 0), Some(nextA), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 0), Some(nextA), 1000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence == 0.0 && score.outgoingConfidence === (2.0 / 4) +- 0.001 }, max = 60 seconds) // Different origin node - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(randomKey().publicKey, 7), Some(randomKey().publicKey), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(randomKey().publicKey, 7), Some(randomKey().publicKey), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 }) // Very large HTLC - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 100000000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 100000000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === 0.0 +- 0.001 && score.outgoingConfidence === 0.0 +- 0.001 @@ -116,54 +116,54 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa val (d, e) = (randomKey().publicKey, randomKey().publicKey) val upstream1 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 7, 20000 msat) :: makeChannelUpstream(b, 7, 40000 msat) :: makeChannelUpstream(c, 0, 10000 msat) :: makeChannelUpstream(c, 2, 20000 msat) :: makeChannelUpstream(c, 2, 30000 msat) :: Nil) - reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(d), 12000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(d), 12000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 }) - val added1 = makeOutgoingHtlcAdded(upstream1, d, 6000 msat) + val added1 = makeOutgoingHtlcAdded(upstream1, d, 6000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added1) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added1.add)) val upstream2 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 7, 10000 msat) :: makeChannelUpstream(c, 0, 10000 msat) :: Nil) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(d), 2000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(d), 2000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (1.0 / 3) +- 0.001 && score.outgoingConfidence === (6.0 / 10) +- 0.001 }, max = 60 seconds) - val added2 = makeOutgoingHtlcAdded(upstream2, d, 2000 msat) + val added2 = makeOutgoingHtlcAdded(upstream2, d, 2000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added2) val upstream3 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 0, 10000 msat) :: makeChannelUpstream(b, 7, 15000 msat) :: makeChannelUpstream(b, 7, 5000 msat) :: Nil) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(e), 3000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(e), 3000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 }, max = 60 seconds) - val added3 = makeOutgoingHtlcAdded(upstream3, e, 3000 msat) + val added3 = makeOutgoingHtlcAdded(upstream3, e, 3000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added3) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFailed(added2.add)) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added3.add)) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(d), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (2.0 / 4) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 }, max = 60 seconds) - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 0), Some(d), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 0), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (1.0 / 3) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 }) - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 7), Some(d), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 7), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (4.0 / 6) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 }) - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 0), Some(e), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 0), Some(e), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence == 0.0 && score.outgoingConfidence === (3.0 / 5) +- 0.001 }) - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(c, 0), Some(e), 1000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(c, 0), Some(e), 1000 msat, BlockHeight(0), CltvExpiry(2)) assert({ val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence === (3.0 / 5) +- 0.001 && score.outgoingConfidence === (3.0 / 5) +- 0.001 @@ -178,30 +178,30 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // Our peer builds a good reputation by sending successful endorsed payments for (_ <- 1 to 200) { val upstream = makeChannelUpstream(originNode, 7) - val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat) + val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added.add)) } awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] (score.incomingConfidence === 1.0 +- 0.01) && (score.outgoingConfidence === 1.0 +- 0.01) }, max = 60 seconds) // HTLCs with lower endorsement don't benefit from this high reputation. for (endorsement <- 0 to 6) { - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, endorsement), Some(nextNode), 10000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, endorsement), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) assert(replyTo.expectMessageType[Reputation.Score].incomingConfidence == 0.0) } // The attack starts, HTLCs stay pending. for (_ <- 1 to 100) { val upstream = makeChannelUpstream(originNode, 7) - val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat) + val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added) } awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) replyTo.expectMessageType[Reputation.Score].incomingConfidence < 1.0 / 2 }, max = 60 seconds) } @@ -215,11 +215,11 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // A, B and C are good nodes with a good reputation. for (node <- Seq(a, b, c)) { val upstream = makeChannelUpstream(node, 7) - val added = makeOutgoingHtlcAdded(upstream, randomKey().publicKey, 10000000 msat) + val added = makeOutgoingHtlcAdded(upstream, randomKey().publicKey, 10000000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added.add)) awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(node, 7), Some(attacker), 10000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(node, 7), Some(attacker), 10000 msat, BlockHeight(0), CltvExpiry(2)) val score = replyTo.expectMessageType[Reputation.Score] score.incomingConfidence > 0.9 && score.outgoingConfidence == 0.0 }, max = 60 seconds) @@ -229,7 +229,7 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa for (node <- Seq(a, b, c)) { for (_ <- 1 to 100) { val upstream = makeChannelUpstream(node, 7) - val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat) + val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added) testKit.system.eventStream ! EventStream.Publish(makeOutgoingHtlcFulfilled(added.add)) } @@ -239,12 +239,12 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa for (node <- Seq(a, b, c)) { for (_ <- 1 to 50) { val upstream = makeChannelUpstream(node, 7) - val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat) + val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat, CltvExpiry(2)) testKit.system.eventStream ! EventStream.Publish(added) } } awaitCond({ - reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(attacker), 10000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(attacker), 10000 msat, BlockHeight(0), CltvExpiry(2)) replyTo.expectMessageType[Reputation.Score].outgoingConfidence < 1.0 / 2 }, max = 60 seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala index e0748cf0b9..efcc7ff7b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala @@ -17,80 +17,96 @@ package fr.acinq.eclair.reputation import fr.acinq.eclair.reputation.Reputation._ -import fr.acinq.eclair.{MilliSatoshiLong, TimestampMilli, randomBytes32} +import fr.acinq.eclair.wire.protocol.{TlvStream, UpdateAddHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshiLong, TimestampMilli, randomBytes32, randomLong} import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.duration.DurationInt class ReputationSpec extends AnyFunSuite { - val (htlcId1, htlcId2, htlcId3, htlcId4, htlcId5, htlcId6, htlcId7) = (HtlcId(randomBytes32(), 1), HtlcId(randomBytes32(), 2), HtlcId(randomBytes32(), 3), HtlcId(randomBytes32(), 4), HtlcId(randomBytes32(), 5), HtlcId(randomBytes32(), 6), HtlcId(randomBytes32(), 7)) + def makeAdd(expiry: CltvExpiry): UpdateAddHtlc = UpdateAddHtlc(randomBytes32(), randomLong(), 100000 msat, randomBytes32(), expiry, null, TlvStream.empty) test("basic, single endorsement level") { - var r = Reputation.init(Config(enabled = true, 1 day, 1 second, 2)) - assert(r.getConfidence(10000 msat, 0) == 0) - r = r.addPendingHtlc(htlcId1, 10000 msat, 0) - r = r.settlePendingHtlc(htlcId1, isSuccess = true) - assert(r.getConfidence(10000 msat, 0) === (1.0 / 3) +- 0.001) - r = r.addPendingHtlc(htlcId2, 10000 msat, 0) - assert(r.getConfidence(10000 msat, 0) === (1.0 / 5) +- 0.001) - r = r.addPendingHtlc(htlcId3, 10000 msat, 0) - r = r.settlePendingHtlc(htlcId2, isSuccess = true) - r = r.settlePendingHtlc(htlcId3, isSuccess = true) - assert(r.getConfidence(1 msat, 0) === 1.0 +- 0.001) - r = r.addPendingHtlc(htlcId4, 1 msat, 0) - assert(r.getConfidence(40000 msat, 0) === (3.0 / 11) +- 0.001) - r = r.addPendingHtlc(htlcId5, 40000 msat, 0) - assert(r.getConfidence(10000 msat, 0) === (3.0 / 13) +- 0.001) - r = r.addPendingHtlc(htlcId6, 10000 msat, 0) - r = r.settlePendingHtlc(htlcId6, isSuccess = false) - assert(r.getConfidence(10000 msat, 0) === (3.0 / 13) +- 0.001) + var r = Reputation.init(Config(enabled = true, 1 day, 10 minutes)) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(5)) == 0) + val add1 = makeAdd(CltvExpiry(5)) + r = r.addPendingHtlc(add1, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (1.0 / 3) +- 0.001) + val add2 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add2, 10000 msat, 0) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(3)) === (1.0 / 6) +- 0.001) + val add3 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add3, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = true) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true) + assert(r.getConfidence(1 msat, 0, BlockHeight(0), CltvExpiry(4)) === 1.0 +- 0.001) + val add4 = makeAdd(CltvExpiry(4)) + r = r.addPendingHtlc(add4, 1 msat, 0) + assert(r.getConfidence(40000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (3.0 / 11) +- 0.001) + val add5 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add5, 40000 msat, 0) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(3)) === (3.0 / 14) +- 0.001) + val add6 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add6, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add6), isSuccess = false) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (3.0 / 13) +- 0.001) } test("long HTLC, single endorsement level") { - var r = Reputation.init(Config(enabled = true, 1000 day, 1 second, 10)) - assert(r.getConfidence(100000 msat, 1, TimestampMilli(0)) == 0) - r = r.addPendingHtlc(htlcId1, 100000 msat, 1, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId1, isSuccess = true, now = TimestampMilli(0)) - assert(r.getConfidence(1000 msat, 1, TimestampMilli(0)) === (10.0 / 11) +- 0.001) - r = r.addPendingHtlc(htlcId2, 1000 msat, 1, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId2, isSuccess = false, now = TimestampMilli(0) + 100.seconds) - assert(r.getConfidence(0 msat, 1, now = TimestampMilli(0) + 100.seconds) === 0.5 +- 0.001) + var r = Reputation.init(Config(enabled = true, 1000 day, 1 minute)) + assert(r.getConfidence(100000 msat, 1, BlockHeight(0), CltvExpiry(6), TimestampMilli(0)) == 0) + val add1 = makeAdd(CltvExpiry(6)) + r = r.addPendingHtlc(add1, 100000 msat, 1, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 1, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) === (10.0 / 11) +- 0.001) + val add2 = makeAdd(CltvExpiry(1)) + r = r.addPendingHtlc(add2, 1000 msat, 1, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = false, now = TimestampMilli(0) + 100.minutes) + assert(r.getConfidence(0 msat, 1, BlockHeight(0), CltvExpiry(1), now = TimestampMilli(0) + 100.minutes) === 0.5 +- 0.001) } test("exponential decay, single endorsement level") { - var r = Reputation.init(Config(enabled = true, 100 seconds, 1 second, 1)) - r = r.addPendingHtlc(htlcId1, 1000 msat, 2, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId1, isSuccess = true, now = TimestampMilli(0)) - assert(r.getConfidence(1000 msat, 2, TimestampMilli(0)) == 1.0 / 2) - r = r.addPendingHtlc(htlcId2, 1000 msat, 2, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId2, isSuccess = true, now = TimestampMilli(0)) - assert(r.getConfidence(1000 msat, 2, TimestampMilli(0)) == 2.0 / 3) - r = r.addPendingHtlc(htlcId3, 1000 msat, 2, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId3, isSuccess = true, now = TimestampMilli(0)) - assert(r.getConfidence(1000 msat, 2, TimestampMilli(0) + 100.seconds) == 1.5 / 2.5) - assert(r.getConfidence(1000 msat, 2, TimestampMilli(0) + 1.hour) < 0.000001) + var r = Reputation.init(Config(enabled = true, 100 seconds, 10 minutes)) + val add1 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add1, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 1.0 / 2) + val add2 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add2, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 2.0 / 3) + val add3 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add3, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 100.seconds) == 1.5 / 2.5) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 1.hour) < 0.000001) } test("multiple endorsement levels") { - var r = Reputation.init(Config(enabled = true, 1 day, 1 second, 10)) - assert(r.getConfidence(1 msat, 7, TimestampMilli(0)) == 0) - r = r.addPendingHtlc(htlcId1, 100000 msat, 0, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId1, isSuccess = true, TimestampMilli(0)) - r = r.addPendingHtlc(htlcId2, 900000 msat, 0, TimestampMilli(0)) - r = r.settlePendingHtlc(htlcId2, isSuccess = false, TimestampMilli(1000)) - r = r.addPendingHtlc(htlcId3, 50000 msat, 4, TimestampMilli(1000)) - r = r.settlePendingHtlc(htlcId3, isSuccess = true, TimestampMilli(1000)) - r = r.addPendingHtlc(htlcId4, 50000 msat, 4, TimestampMilli(1000)) - r = r.settlePendingHtlc(htlcId4, isSuccess = false, TimestampMilli(2000)) - assert(r.getConfidence(1 msat, 0, TimestampMilli(2000)) === 0.1 +- 0.001) - assert(r.getConfidence(1 msat, 4, TimestampMilli(2000)) === 0.5 +- 0.001) - assert(r.getConfidence(1 msat, 7, TimestampMilli(2000)) === 0.5 +- 0.001) - assert(r.getConfidence(1000 msat, 0, TimestampMilli(2000)) === 0.1 +- 0.001) - assert(r.getConfidence(1000 msat, 4, TimestampMilli(2000)) === 5.0 / 11 +- 0.001) - assert(r.getConfidence(1000 msat, 7, TimestampMilli(2000)) === 5.0 / 11 +- 0.001) - assert(r.getConfidence(100000 msat, 0, TimestampMilli(2000)) === 0.05 +- 0.001) - assert(r.getConfidence(100000 msat, 4, TimestampMilli(2000)) === 1.0 / 14 +- 0.001) - assert(r.getConfidence(100000 msat, 7, TimestampMilli(2000)) === 1.0 / 14 +- 0.001) + var r = Reputation.init(Config(enabled = true, 1 day, 1 minute)) + assert(r.getConfidence(1 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 0) + val add1 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add1, 100000 msat, 0, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, TimestampMilli(0)) + val add2 = makeAdd(CltvExpiry(4)) + r = r.addPendingHtlc(add2, 900000 msat, 0, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = false, TimestampMilli(0) + 1.minute) + val add3 = makeAdd(CltvExpiry(5)) + r = r.addPendingHtlc(add3, 50000 msat, 4, TimestampMilli(0) + 1.minute) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true, TimestampMilli(0) + 1.minute) + val add4 = makeAdd(CltvExpiry(6)) + r = r.addPendingHtlc(add4, 50000 msat, 4, TimestampMilli(0) + 1.minute) + r = r.settlePendingHtlc(HtlcId(add4), isSuccess = false, TimestampMilli(0) + 2.minutes) + assert(r.getConfidence(1 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.1 +- 0.01) + assert(r.getConfidence(1 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.5 +- 0.01) + assert(r.getConfidence(1 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.5 +- 0.01) + assert(r.getConfidence(1000 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.1 +- 0.01) + assert(r.getConfidence(1000 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 5.0 / 11 +- 0.01) + assert(r.getConfidence(1000 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 5.0 / 11 +- 0.01) + assert(r.getConfidence(100000 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.05 +- 0.01) + assert(r.getConfidence(100000 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 1.0 / 14 +- 0.01) + assert(r.getConfidence(100000 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 1.0 / 14 +- 0.01) } } \ No newline at end of file