What changed, and why it matters
This commit changes how Eclair estimates the trustworthiness (reputation) of other Lightning nodes when deciding whether to forward payments. Previously, pending payments were penalized using a fixed multiplier that did not reflect how long an HTLC could actually stay locked. Now the penalty is based on the real CLTV expiry (the deadline by which the payment must settle on-chain), assuming each block takes about 10 minutes. The change also increases the half-life of reputation data from 15 to 30 days and raises the 'max-relay-duration' threshold from 12 seconds to 5 minutes. It is a defensive hardening change against channel-jamming attacks, not a fix for an active exploit or a disclosed vulnerability.
Review the new CLTV-based weight formula for numerical stability and edge cases (e.g., expiry at or below current block height, very large CLTV deltas). Ensure the 10-minute/block assumption and the 5-minute max-relay-duration threshold are acceptable for your node's risk profile. Run the updated reputation tests and consider monitoring reputation scores after upgrade.
Security signals we found
Anti-jamming / reputation-system hardening
Removal of fixed pending-multiplier in favor of CLTV-based expected settlement time
Configuration change: half-life 15 days -> 30 days, max-relay-duration 12 seconds -> 5 minutes
Reputation scoring now depends on current block height and HTLC expiry
No memory-safety, cryptographic, or remote-code-execution signals in diff
Evidence from the diff
The patch removes the fixed pending-multiplier from Reputation.Config and Reputation, and instead computes pending-HTLC weight using the actual remaining CLTV expiry: weight = fee * ((alreadyPending + (expiry - currentBlockHeight)*10.minutes) / maxRelayDuration). Settled HTLCs now use a min-duration floor of 1.0 for successes. Callers in ChannelRelay, PaymentLifecycle and ReputationRecorder pass currentBlockHeight and expiry to GetConfidence. Default half-life doubled to 30 days and max-relay-duration raised to 5 minutes to compensate for the much larger dynamic range of CLTV-based weights. Tests and reference.conf are updated accordingly.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scalaeclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scalaeclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scalaeclair-core/src/main/resources/reference.confInspect captured patch +161 / −141
diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md
index a3bf9d0..6d3f046 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 2bce28b..2c68f5d 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 9ca454c..aab99e4 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 86ab8a8..8c3b574 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 b69d6d6..d4c1ae4 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 d673248..aa564e0 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 aee51b7..adf9ffb 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 cecc448..f480a92 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 f01d379..f6ea6ef 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 5e6ba0a..61eeb64 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 e0748cf..efcc7ff 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
Why this scored 41/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.