diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index bd09eee4bc..283c22dbc0 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -501,6 +501,7 @@ eclair { local-reputation { max-weight-msat = 100000000000 # 1 BTC min-duration = 12 seconds + pending-multiplier = 1000 } } 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 b64a9ac304..e032a6e73c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -601,7 +601,9 @@ object NodeParams extends Logging { purgeInvoicesInterval = purgeInvoicesInterval, localReputationConfig = ReputationConfig(MilliSatoshi( config.getLong("local-reputation.max-weight-msat")), - FiniteDuration(config.getDuration("local-reputation.min-duration").getSeconds, TimeUnit.SECONDS)), + FiniteDuration(config.getDuration("local-reputation.min-duration").getSeconds, TimeUnit.SECONDS), + config.getDouble("local-reputation.pending-multiplier") + ), ) } } 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 33242c3ccd..2bf4044d2f 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 @@ -22,8 +22,8 @@ import fr.acinq.eclair.{MilliSatoshi, TimestampMilli} import java.util.UUID import scala.concurrent.duration.FiniteDuration -case class Reputation(pastWeight: Double, pending: Map[UUID, Pending], pastScore: Double, maxWeight: Double, minDuration: FiniteDuration) { - private def pendingWeight(now: TimestampMilli): Double = pending.values.map(_.weight(now, minDuration)).sum +case class Reputation(pastWeight: Double, pending: Map[UUID, Pending], pastScore: Double, maxWeight: Double, minDuration: FiniteDuration, pendingMultiplier: Double) { + private def pendingWeight(now: TimestampMilli): Double = pending.values.map(_.weight(now, minDuration, pendingMultiplier)).sum def confidence(now: TimestampMilli = TimestampMilli.now()): Double = pastScore / (pastWeight + pendingWeight(now)) @@ -35,25 +35,25 @@ case class Reputation(pastWeight: Double, pending: Map[UUID, Pending], pastScore def record(relayId: UUID, isSuccess: Boolean, feeOverride: Option[MilliSatoshi] = None, now: TimestampMilli = TimestampMilli.now()): Reputation = { var p = pending.getOrElse(relayId, Pending(MilliSatoshi(0), now)) feeOverride.foreach(fee => p = p.copy(fee = fee)) - val newWeight = pastWeight + p.weight(now, minDuration) + val newWeight = pastWeight + p.weight(now, minDuration, 1.0) val newScore = if (isSuccess) pastScore + p.fee.toLong.toDouble else pastScore if (newWeight > maxWeight) { - Reputation(maxWeight, pending - relayId, newScore * maxWeight / newWeight, maxWeight, minDuration) + Reputation(maxWeight, pending - relayId, newScore * maxWeight / newWeight, maxWeight, minDuration, pendingMultiplier) } else { - Reputation(newWeight, pending - relayId, newScore, maxWeight, minDuration) + Reputation(newWeight, pending - relayId, newScore, maxWeight, minDuration, pendingMultiplier) } } } object Reputation { case class Pending(fee: MilliSatoshi, startedAt: TimestampMilli) { - def weight(now: TimestampMilli, minDuration: FiniteDuration): Double = { + def weight(now: TimestampMilli, minDuration: FiniteDuration, pendingMultiplier: Double): Double = { val duration = now - startedAt - fee.toLong.toDouble * (duration / minDuration).max(1.0) + fee.toLong.toDouble * (duration / minDuration).max(pendingMultiplier) } } - case class ReputationConfig(maxWeight: MilliSatoshi, minDuration: FiniteDuration) + case class ReputationConfig(maxWeight: MilliSatoshi, minDuration: FiniteDuration, pendingMultiplier: Double) - def init(config: ReputationConfig): Reputation = Reputation(0.0, Map.empty, 0.0, config.maxWeight.toLong.toDouble, config.minDuration) + def init(config: ReputationConfig): Reputation = Reputation(0.0, Map.empty, 0.0, config.maxWeight.toLong.toDouble, config.minDuration, config.pendingMultiplier) } 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 de610d4456..99f9c16512 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -221,7 +221,7 @@ object TestConstants { maxAttempts = 2, ), purgeInvoicesInterval = None, - localReputationConfig = ReputationConfig(1000000 msat, 10 seconds), + localReputationConfig = ReputationConfig(1000000 msat, 10 seconds, 100), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( @@ -382,7 +382,7 @@ object TestConstants { maxAttempts = 2, ), purgeInvoicesInterval = None, - localReputationConfig = ReputationConfig(2000000 msat, 20 seconds), + localReputationConfig = ReputationConfig(2000000 msat, 20 seconds, 200), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index 476c7c46c5..97ca270d85 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -147,7 +147,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // initiator should reject commands that change the commitment once it became quiescent val sender1, sender2, sender3 = TestProbe() val cmds = Seq( - CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, localOrigin(sender1.ref)), + CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, localOrigin(sender1.ref)), CMD_UPDATE_FEE(FeeratePerKw(100 sat), replyTo_opt = Some(sender2.ref)), CMD_CLOSE(sender3.ref, None, None) ) @@ -163,7 +163,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // both should reject commands that change the commitment while quiescent val sender1, sender2, sender3 = TestProbe() val cmds = Seq( - CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, localOrigin(sender1.ref)), + CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, localOrigin(sender1.ref)), CMD_UPDATE_FEE(FeeratePerKw(100 sat), replyTo_opt = Some(sender2.ref)), CMD_CLOSE(sender3.ref, None, None) ) @@ -302,7 +302,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ val (preimage, add) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - alice2relayer.expectMsg(RelayForward(add)) + alice2relayer.expectMsg(RelayForward(add, TestConstants.Bob.nodeParams.nodeId)) initiateQuiescence(f, sendInitialStfu = true) val forbiddenMsg = UpdateFulfillHtlc(channelId(bob), add.id, preimage) // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) @@ -343,7 +343,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ initiateQuiescence(f, sendInitialStfu = true) // have to build a htlc manually because eclair would refuse to accept this command as it's forbidden - val forbiddenMsg = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, blinding_opt = None) + val forbiddenMsg = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, blinding_opt = None, confidence = 1.0) // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) bob2alice.forward(alice, forbiddenMsg) alice2bob.expectMsg(Warning(channelId(alice), ForbiddenDuringSplice(channelId(alice), "UpdateAddHtlc").getMessage)) 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 6999c36457..a5b8bbb19b 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 @@ -36,7 +36,7 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa case class FixtureParam(config: ReputationConfig, reputationRecorder: ActorRef[Command], replyTo: TestProbe[Confidence]) override def withFixture(test: OneArgTest): Outcome = { - val config = ReputationConfig(1000000000 msat, 10 seconds) + val config = ReputationConfig(1000000000 msat, 10 seconds, 2) val replyTo = TestProbe[Confidence]("confidence") val reputationRecorder = testKit.spawn(ReputationRecorder(config, Map.empty)) withFixture(test.toNoArgTest(FixtureParam(config, reputationRecorder.ref, replyTo))) @@ -45,26 +45,26 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa test("standard") { f => import f._ - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid1, 1100 msat) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid1, 2000 msat) assert(replyTo.expectMessageType[Confidence].value == 0) reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid1, isSuccess = true) - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid2, 1100 msat) - assert(replyTo.expectMessageType[Confidence].value == 0.5) - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid3, 1100 msat) - assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid2, 1000 msat) + assert(replyTo.expectMessageType[Confidence].value === (2.0 / 4) +- 0.001) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid3, 3000 msat) + assert(replyTo.expectMessageType[Confidence].value === (2.0 / 10) +- 0.001) reputationRecorder ! CancelRelay(originNode, isEndorsed = true, uuid3) - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid4, 1100 msat) - assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid4, 1000 msat) + assert(replyTo.expectMessageType[Confidence].value === (2.0 / 6) +- 0.001) reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid4, isSuccess = true) reputationRecorder ! RecordResult(originNode, isEndorsed = true, uuid2, isSuccess = false) // Not endorsed - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = false, uuid5, 1100 msat) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = false, uuid5, 1000 msat) assert(replyTo.expectMessageType[Confidence].value == 0) // Different origin node - reputationRecorder ! GetConfidence(replyTo.ref, randomKey().publicKey, isEndorsed = true, uuid6, 1100 msat) + reputationRecorder ! GetConfidence(replyTo.ref, randomKey().publicKey, isEndorsed = true, uuid6, 1000 msat) assert(replyTo.expectMessageType[Confidence].value == 0) // Very large HTLC - reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid5, 10000000 msat) + reputationRecorder ! GetConfidence(replyTo.ref, originNode, isEndorsed = true, uuid5, 100000000 msat) assert(replyTo.expectMessageType[Confidence].value === 0.0 +- 0.001) } @@ -73,25 +73,25 @@ class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (b, true) -> 2000.msat, (c, false) -> 3000.msat), uuid1) + reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 2000.msat, (b, true) -> 4000.msat, (c, false) -> 6000.msat), uuid1) assert(replyTo.expectMessageType[Confidence].value == 0) - reputationRecorder ! RecordTrampolineSuccess(Map((a, true) -> 500.msat, (b, true) -> 1000.msat, (c, false) -> 1500.msat), uuid1) + reputationRecorder ! RecordTrampolineSuccess(Map((a, true) -> 1000.msat, (b, true) -> 2000.msat, (c, false) -> 3000.msat), uuid1) reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, true) -> 1000.msat, (c, false) -> 1000.msat), uuid2) - assert(replyTo.expectMessageType[Confidence].value === 0.333 +- 0.001) + assert(replyTo.expectMessageType[Confidence].value === (1.0 / 3) +- 0.001) reputationRecorder ! GetTrampolineConfidence(replyTo.ref, Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3) assert(replyTo.expectMessageType[Confidence].value == 0) reputationRecorder ! RecordTrampolineFailure(Set((a, true), (c, false)), uuid2) reputationRecorder ! RecordTrampolineSuccess(Map((a, false) -> 1000.msat, (b, true) -> 2000.msat), uuid3) reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = true, uuid4, 1000 msat) - assert(replyTo.expectMessageType[Confidence].value === 0.2 +- 0.001) + assert(replyTo.expectMessageType[Confidence].value === (1.0 / 4) +- 0.001) reputationRecorder ! GetConfidence(replyTo.ref, a, isEndorsed = false, uuid5, 1000 msat) - assert(replyTo.expectMessageType[Confidence].value === 0.5 +- 0.001) + assert(replyTo.expectMessageType[Confidence].value === (1.0 / 3) +- 0.001) reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = true, uuid6, 1000 msat) - assert(replyTo.expectMessageType[Confidence].value === 0.75 +- 0.001) + assert(replyTo.expectMessageType[Confidence].value === (4.0 / 6) +- 0.001) reputationRecorder ! GetConfidence(replyTo.ref, b, isEndorsed = false, uuid7, 1000 msat) assert(replyTo.expectMessageType[Confidence].value == 0.0) reputationRecorder ! GetConfidence(replyTo.ref, c, isEndorsed = false, uuid8, 1000 msat) - assert(replyTo.expectMessageType[Confidence].value === (3.0 / 7) +- 0.001) + assert(replyTo.expectMessageType[Confidence].value === (3.0 / 6) +- 0.001) } } \ No newline at end of file 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 88bb33a4c5..f919079359 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 @@ -28,63 +28,64 @@ class ReputationSpec extends AnyFunSuite { val (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6, uuid7) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) test("basic") { - var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second)) + var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second, 2)) r = r.attempt(uuid1, 10000 msat) assert(r.confidence() == 0) r = r.record(uuid1, isSuccess = true) r = r.attempt(uuid2, 10000 msat) - assert(r.confidence() == 0.5) + assert(r.confidence() === (1.0 / 3) +- 0.001) r = r.attempt(uuid3, 10000 msat) - assert(r.confidence() === 0.333 +- 0.001) + assert(r.confidence() === (1.0 / 5) +- 0.001) r = r.record(uuid2, isSuccess = true) r = r.record(uuid3, isSuccess = true) + assert(r.confidence() == 1) r = r.attempt(uuid4, 1 msat) assert(r.confidence() === 1.0 +- 0.001) - r = r.attempt(uuid5, 90000 msat) - assert(r.confidence() === 0.25 +- 0.001) + r = r.attempt(uuid5, 40000 msat) + assert(r.confidence() === (3.0 / 11) +- 0.001) r = r.attempt(uuid6, 10000 msat) assert(r.confidence() === (3.0 / 13) +- 0.001) r = r.cancel(uuid5) - assert(r.confidence() === 0.75 +- 0.001) + assert(r.confidence() === (3.0 / 5) +- 0.001) r = r.record(uuid6, isSuccess = false) - assert(r.confidence() === 0.75 +- 0.001) + assert(r.confidence() === (3.0 / 4) +- 0.001) r = r.attempt(uuid7, 10000 msat) - assert(r.confidence() === 0.6 +- 0.001) + assert(r.confidence() === (3.0 / 6) +- 0.001) } test("long HTLC") { - var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second)) + var r = Reputation.init(ReputationConfig(1000000000 msat, 1 second, 10)) r = r.attempt(uuid1, 100000 msat) assert(r.confidence() == 0) r = r.record(uuid1, isSuccess = true) assert(r.confidence() == 1) r = r.attempt(uuid2, 1000 msat, TimestampMilli(0)) - assert(r.confidence(TimestampMilli(0)) === 0.99 +- 0.001) + assert(r.confidence(TimestampMilli(0)) === (10.0 / 11) +- 0.001) assert(r.confidence(TimestampMilli(0) + 100.seconds) == 0.5) r = r.record(uuid2, isSuccess = false, now = TimestampMilli(0) + 100.seconds) assert(r.confidence() == 0.5) } test("max weight") { - var r = Reputation.init(ReputationConfig(1000000 msat, 1 second)) + var r = Reputation.init(ReputationConfig(100 msat, 1 second, 10)) // build perfect reputation for(i <- 1 to 100){ val uuid = UUID.randomUUID() - r = r.attempt(uuid, 100000 msat) + r = r.attempt(uuid, 10 msat) r = r.record(uuid, isSuccess = true) } assert(r.confidence() == 1) - r = r.attempt(uuid1, 100000 msat) - assert(r.confidence() === 0.91 +- 0.01) + r = r.attempt(uuid1, 1 msat) + assert(r.confidence() === (100.0 / 110) +- 0.001) r = r.record(uuid1, isSuccess = false) - assert(r.confidence() === 0.91 +- 0.01) - r = r.attempt(uuid2, 100000 msat) - assert(r.confidence() === 0.83 +- 0.01) + assert(r.confidence() === (100.0 / 101) +- 0.001) + r = r.attempt(uuid2, 1 msat) + assert(r.confidence() === (100.0 / 101) * (100.0 / 110) +- 0.001) r = r.record(uuid2, isSuccess = false) - assert(r.confidence() === 0.83 +- 0.01) - r = r.attempt(uuid3, 100000 msat) - assert(r.confidence() === 0.75 +- 0.01) + assert(r.confidence() === (100.0 / 101) * (100.0 / 101) +- 0.001) + r = r.attempt(uuid3, 1 msat) + assert(r.confidence() === (100.0 / 101) * (100.0 / 101) * (100.0 / 110) +- 0.001) r = r.record(uuid3, isSuccess = false) - assert(r.confidence() === 0.75 +- 0.01) + assert(r.confidence() === (100.0 / 101) * (100.0 / 101) * (100.0 / 101) +- 0.001) } } \ No newline at end of file