Skip to content

Commit

Permalink
pending multiplier
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Aug 10, 2023
1 parent 0933d8d commit 14b6dd1
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 55 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 @@ -501,6 +501,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 @@ -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")
),
)
}
}
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 @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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)
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
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 14b6dd1

Please sign in to comment.