Skip to content

Commit

Permalink
pending multiplier
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Apr 17, 2024
1 parent 4751c52 commit 5a99c9f
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 52 deletions.
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ eclair {
local-reputation {
max-weight-msat = 100000000000 # 1 BTC
min-duration = 12 seconds
pending-multiplier = 1000
}
}

Expand Down
4 changes: 3 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,9 @@ object NodeParams extends Logging {
),
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")
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ object TestConstants {
),
purgeInvoicesInterval = None,
revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis),
localReputationConfig = ReputationConfig(1000000 msat, 10 seconds),
localReputationConfig = ReputationConfig(1000000 msat, 10 seconds, 100),
)

def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams(
Expand Down Expand Up @@ -397,7 +397,7 @@ object TestConstants {
),
purgeInvoicesInterval = None,
revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis),
localReputationConfig = ReputationConfig(2000000 msat, 20 seconds),
localReputationConfig = ReputationConfig(2000000 msat, 20 seconds, 200),
)

def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
import f._
val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
alice2relayer.expectMsg(RelayForward(add, bob.underlyingActor.nodeParams.nodeId))
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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)
}

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit 5a99c9f

Please sign in to comment.