diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ba848850c..323da8820 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -237,6 +237,18 @@ eclair { // Number of blocks before the incoming HTLC expires that an async payment must be triggered by the receiver cancel-safety-before-timeout-blocks = 144 } + + // We assign reputations to our peers to prioritize HTLCs during congestion. + // The reputation is computed as fees paid divided by what should have been paid if all HTLCs were successful. + peer-reputation { + // Reputation decays with the following half life to emphasize recent behavior. + half-life = 7 days + // HTLCs that stay pending for longer than this get penalized + max-htlc-relay-duration = 12 seconds + // Pending HTLCs are counted as failed, and because they could potentially stay pending for a very long time, the + // following multiplier is applied. + pending-multiplier = 1000 // A pending HTLCs counts as a thousand failed ones. + } } on-chain-fees { @@ -547,15 +559,6 @@ eclair { enabled = true // enable automatic purges of expired invoices from the database interval = 24 hours // interval between expired invoice purges } - - local-reputation { - # Reputation decays with the following half life to emphasize recent behavior. - half-life = 7 days - # HTLCs that stay pending for longer than this get penalized - good-htlc-duration = 12 seconds # 95% of successful payments settle in less than 12 seconds, only the slowest 5% will be penalized. - # How much to penalize pending HLTCs. A pending HTLC is considered equivalent to this many fast-failing HTLCs. - pending-multiplier = 1000 - } } akka { 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 0dc619b84..92d6cb54f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -88,8 +88,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, blockchainWatchdogSources: Seq[String], onionMessageConfig: OnionMessageConfig, purgeInvoicesInterval: Option[FiniteDuration], - revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config, - localReputationConfig: ReputationConfig) { + revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -563,7 +562,12 @@ object NodeParams extends Logging { privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")), minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")), enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS), - asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks) + asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks), + peerReputationConfig = ReputationConfig( + FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS), + FiniteDuration(config.getDuration("relay.peer-reputation.max-htlc-relay-duration").getSeconds, TimeUnit.SECONDS), + config.getDouble("relay.peer-reputation.pending-multiplier"), + ), ), db = database, autoReconnect = config.getBoolean("auto-reconnect"), @@ -613,12 +617,7 @@ object NodeParams extends Logging { revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config( batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"), interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS) - ), - localReputationConfig = ReputationConfig( - FiniteDuration(config.getDuration("local-reputation.half-life").getSeconds, TimeUnit.SECONDS), - FiniteDuration(config.getDuration("local-reputation.good-htlc-duration").getSeconds, TimeUnit.SECONDS), - config.getDouble("local-reputation.pending-multiplier"), - ), + ) ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 93a5e184c..2c0453554 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -361,7 +361,7 @@ class Setup(val datadir: File, offerManager = system.spawn(Behaviors.supervise(OfferManager(nodeParams, router, paymentTimeout = 1 minute)).onFailure(typed.SupervisorStrategy.resume), name = "offer-manager") paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler", SupervisorStrategy.Resume)) triggerer = system.spawn(Behaviors.supervise(AsyncPaymentTriggerer()).onFailure(typed.SupervisorStrategy.resume), name = "async-payment-triggerer") - reputationRecorder = system.spawn(Behaviors.supervise(ReputationRecorder(nodeParams.localReputationConfig, Map.empty)).onFailure(typed.SupervisorStrategy.resume), name = "reputation-recorder") + reputationRecorder = system.spawn(Behaviors.supervise(ReputationRecorder(nodeParams.relayParams.peerReputationConfig, Map.empty)).onFailure(typed.SupervisorStrategy.resume), name = "reputation-recorder") relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, triggerer, reputationRecorder, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) _ = relayer ! PostRestartHtlcCleaner.Init(channels) // Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index e67b8f702..51a53016b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -28,6 +28,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ +import fr.acinq.eclair.reputation.Reputation.ReputationConfig import fr.acinq.eclair.reputation.ReputationRecorder import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams} @@ -136,7 +137,8 @@ object Relayer extends Logging { privateChannelFees: RelayFees, minTrampolineFees: RelayFees, enforcementDelay: FiniteDuration, - asyncPaymentsParams: AsyncPaymentsParams) { + asyncPaymentsParams: AsyncPaymentsParams, + peerReputationConfig: ReputationConfig) { def defaultFees(announceChannel: Boolean): RelayFees = { if (announceChannel) { publicChannelFees 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 1cbbfd291..b2b7e52fb 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 @@ -76,7 +76,7 @@ object Reputation { } } - case class ReputationConfig(halfLife: FiniteDuration, goodDuration: FiniteDuration, pendingMultiplier: Double) + case class ReputationConfig(halfLife: FiniteDuration, maxHtlcRelayDuration: FiniteDuration, pendingMultiplier: Double) - def init(config: ReputationConfig): Reputation = Reputation(0.0, 0.0, TimestampMilli.min, Map.empty, config.halfLife, config.goodDuration, config.pendingMultiplier) + def init(config: ReputationConfig): Reputation = Reputation(0.0, 0.0, TimestampMilli.min, Map.empty, config.halfLife, config.maxHtlcRelayDuration, 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 876a3d3f1..470f2164d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -166,7 +166,9 @@ object TestConstants { feeBase = 548000 msat, feeProportionalMillionths = 30), enforcementDelay = 10 minutes, - asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144))), + asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), + peerReputationConfig = ReputationConfig(1 day, 10 seconds, 100), + ), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, @@ -233,7 +235,6 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - localReputationConfig = ReputationConfig(1 day, 10 seconds, 100), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( @@ -337,7 +338,9 @@ object TestConstants { feeBase = 548000 msat, feeProportionalMillionths = 30), enforcementDelay = 10 minutes, - asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144))), + asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), + peerReputationConfig = ReputationConfig(2 day, 20 seconds, 200), + ), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, @@ -404,7 +407,6 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - localReputationConfig = ReputationConfig(2 days, 20 seconds, 200), ) def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 586babfa3..7ea80b8fa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -67,8 +67,8 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe val bobRegister = system.actorOf(Props(new TestRegister())) val alicePaymentHandler = system.actorOf(Props(new PaymentHandler(aliceParams, aliceRegister, TestProbe().ref))) val bobPaymentHandler = system.actorOf(Props(new PaymentHandler(bobParams, bobRegister, TestProbe().ref))) - val aliceReputationRecorder = system.spawnAnonymous(ReputationRecorder(aliceParams.localReputationConfig, Map.empty)) - val bobReputationRecorder = system.spawnAnonymous(ReputationRecorder(bobParams.localReputationConfig, Map.empty)) + val aliceReputationRecorder = system.spawnAnonymous(ReputationRecorder(aliceParams.relayParams.peerReputationConfig, Map.empty)) + val bobReputationRecorder = system.spawnAnonymous(ReputationRecorder(bobParams.relayParams.peerReputationConfig, Map.empty)) val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler, TestProbe().ref, aliceReputationRecorder)) val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler, TestProbe().ref, bobReputationRecorder)) val wallet = new DummyOnChainWallet() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 4486e1895..cb8ae9a38 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -97,7 +97,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat val router = system.actorOf(Router.props(nodeParams, watcherTyped), "router") val offerManager = system.spawn(OfferManager(nodeParams, router, 1 minute), "offer-manager") val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler") - val reputationRecorder = system.spawn(ReputationRecorder(nodeParams.localReputationConfig, Map.empty), "reputation-recorder") + val reputationRecorder = system.spawn(ReputationRecorder(nodeParams.relayParams.peerReputationConfig, Map.empty), "reputation-recorder") val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler, triggerer.ref.toTyped, reputationRecorder), "relayer") val txPublisherFactory = Channel.SimpleTxPublisherFactory(nodeParams, watcherTyped, bitcoinClient) val channelFactory = Peer.SimpleChannelFactory(nodeParams, watcherTyped, relayer, wallet, txPublisherFactory)