Skip to content

Commit 6e8d675

Browse files
committed
Allow disabling the reputation recorder
We must allow node operators to run plain blip-0004 without our experimental reputation recorder. It's also a stop-gap in case there is a bug in it that can be exploited. We also refactor to introduce more types and documentation, without changing the reputation algorithm itself. We also fix a few issues mentioned on the main PR comments.
1 parent 54f4041 commit 6e8d675

File tree

20 files changed

+193
-141
lines changed

20 files changed

+193
-141
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,28 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
2626

2727
### Local reputation and HTLC endorsement
2828

29-
To protect against jamming attacks, eclair gives a reputation to its neighbors and uses to decide if a HTLC should be relayed given how congested is the outgoing channel.
30-
The reputation is basically how much this node paid us in fees divided by how much they should have paid us for the liquidity and slots that they blocked.
29+
To protect against jamming attacks, eclair gives a reputation to its neighbors and uses it to decide if a HTLC should be relayed given how congested the outgoing channel is.
30+
The reputation is basically how much this node paid us in fees divided by how much they should have paid us for the liquidity and slots that they blocked.
3131
The reputation is per incoming node and endorsement level.
3232
The confidence that the HTLC will be fulfilled is transmitted to the next node using the endorsement TLV of the `update_add_htlc` message.
33+
Note that HTLCs that are considered dangerous are still relayed: this is the first phase of a network-wide experimentation aimed at collecting data.
3334

3435
To configure, edit `eclair.conf`:
36+
3537
```eclair.conf
36-
eclair.local-reputation {
37-
# Reputation decays with the following half life to emphasize recent behavior.
38+
// We assign reputations to our peers to prioritize payments during congestion.
39+
// The reputation is computed as fees paid divided by what should have been paid if all payments were successful.
40+
eclair.peer-reputation {
41+
// Set this parameter to false to disable the reputation algorithm and simply relay the incoming endorsement
42+
// value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md,
43+
enabled = true
44+
// Reputation decays with the following half life to emphasize recent behavior.
3845
half-life = 7 days
39-
# HTLCs that stay pending for longer than this get penalized
40-
good-htlc-duration = 12 seconds
41-
# How much to penalize pending HLTCs. A pending HTLC is considered equivalent to this many fast-failing HTLCs.
42-
pending-multiplier = 1000
46+
// Payments that stay pending for longer than this get penalized
47+
max-relay-duration = 12 seconds
48+
// Pending payments are counted as failed, and because they could potentially stay pending for a very long time,
49+
// the following multiplier is applied.
50+
pending-multiplier = 1000 // A pending payment counts as a thousand failed ones.
4351
}
4452
```
4553

eclair-core/src/main/resources/reference.conf

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,19 @@ eclair {
238238
cancel-safety-before-timeout-blocks = 144
239239
}
240240

241-
// We assign reputations to our peers to prioritize HTLCs during congestion.
242-
// The reputation is computed as fees paid divided by what should have been paid if all HTLCs were successful.
241+
// We assign reputation to our peers to prioritize payments during congestion.
242+
// The reputation is computed as fees paid divided by what should have been paid if all payments were successful.
243243
peer-reputation {
244+
// Set this parameter to false to disable the reputation algorithm and simply relay the incoming endorsement
245+
// value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md,
246+
enabled = true
244247
// Reputation decays with the following half life to emphasize recent behavior.
245248
half-life = 7 days
246-
// HTLCs that stay pending for longer than this get penalized
247-
max-htlc-relay-duration = 12 seconds
248-
// Pending HTLCs are counted as failed, and because they could potentially stay pending for a very long time, the
249-
// following multiplier is applied.
250-
pending-multiplier = 1000 // A pending HTLCs counts as a thousand failed ones.
249+
// Payments that stay pending for longer than this get penalized.
250+
max-relay-duration = 12 seconds
251+
// Pending payments are counted as failed, and because they could potentially stay pending for a very long time,
252+
// the following multiplier is applied.
253+
pending-multiplier = 1000 // A pending payment counts as a thousand failed ones.
251254
}
252255
}
253256

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import fr.acinq.eclair.io.MessageRelay.{RelayAll, RelayChannelsOnly, RelayPolicy
3131
import fr.acinq.eclair.io.PeerConnection
3232
import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig
3333
import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams}
34-
import fr.acinq.eclair.reputation.Reputation.ReputationConfig
34+
import fr.acinq.eclair.reputation.Reputation
3535
import fr.acinq.eclair.router.Announcements.AddressException
3636
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
3737
import fr.acinq.eclair.router.Router._
@@ -563,10 +563,11 @@ object NodeParams extends Logging {
563563
minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")),
564564
enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS),
565565
asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks),
566-
peerReputationConfig = ReputationConfig(
567-
FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS),
568-
FiniteDuration(config.getDuration("relay.peer-reputation.max-htlc-relay-duration").getSeconds, TimeUnit.SECONDS),
569-
config.getDouble("relay.peer-reputation.pending-multiplier"),
566+
peerReputationConfig = Reputation.Config(
567+
enabled = config.getBoolean("relay.peer-reputation.enabled"),
568+
halfLife = FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS),
569+
maxRelayDuration = FiniteDuration(config.getDuration("relay.peer-reputation.max-relay-duration").getSeconds, TimeUnit.SECONDS),
570+
pendingMultiplier = config.getDouble("relay.peer-reputation.pending-multiplier"),
570571
),
571572
),
572573
db = database,

eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,12 @@ class Setup(val datadir: File,
361361
offerManager = system.spawn(Behaviors.supervise(OfferManager(nodeParams, router, paymentTimeout = 1 minute)).onFailure(typed.SupervisorStrategy.resume), name = "offer-manager")
362362
paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler", SupervisorStrategy.Resume))
363363
triggerer = system.spawn(Behaviors.supervise(AsyncPaymentTriggerer()).onFailure(typed.SupervisorStrategy.resume), name = "async-payment-triggerer")
364-
reputationRecorder = system.spawn(Behaviors.supervise(ReputationRecorder(nodeParams.relayParams.peerReputationConfig, Map.empty)).onFailure(typed.SupervisorStrategy.resume), name = "reputation-recorder")
365-
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, triggerer, reputationRecorder, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume))
364+
reputationRecorder_opt = if (nodeParams.relayParams.peerReputationConfig.enabled) {
365+
Some(system.spawn(Behaviors.supervise(ReputationRecorder(nodeParams.relayParams.peerReputationConfig, Map.empty)).onFailure(typed.SupervisorStrategy.resume), name = "reputation-recorder"))
366+
} else {
367+
None
368+
}
369+
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, triggerer, reputationRecorder_opt, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume))
366370
_ = relayer ! PostRestartHtlcCleaner.Init(channels)
367371
// Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system,
368372
// we want to make sure the handler for post-restart broken HTLCs has finished initializing.

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ object ChannelRelay {
4545

4646
// @formatter:off
4747
sealed trait Command
48+
private case object DoRelay extends Command
4849
private case class WrappedConfidence(confidence: Double) extends Command
4950
private case class WrappedForwardFailure(failure: Register.ForwardFailure[CMD_ADD_HTLC]) extends Command
5051
private case class WrappedAddResponse(res: CommandResponse[CMD_ADD_HTLC]) extends Command
@@ -58,9 +59,9 @@ object ChannelRelay {
5859

5960
def apply(nodeParams: NodeParams,
6061
register: ActorRef,
61-
reputationRecorder: typed.ActorRef[ReputationRecorder.StandardCommand],
62+
reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.ChannelRelayCommand]],
6263
channels: Map[ByteVector32, Relayer.OutgoingChannel],
63-
originNode:PublicKey,
64+
originNode: PublicKey,
6465
relayId: UUID,
6566
r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] =
6667
Behaviors.setup { context =>
@@ -70,10 +71,17 @@ object ChannelRelay {
7071
paymentHash_opt = Some(r.add.paymentHash),
7172
nodeAlias_opt = Some(nodeParams.alias))) {
7273
val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), TimestampMilli.now(), originNode)
73-
reputationRecorder ! GetConfidence(context.messageAdapter[ReputationRecorder.Confidence](confidence => WrappedConfidence(confidence.value)), originNode, r.add.endorsement, relayId, r.relayFeeMsat)
74+
reputationRecorder_opt match {
75+
case Some(reputationRecorder) =>
76+
reputationRecorder ! GetConfidence(context.messageAdapter[ReputationRecorder.Confidence](confidence => WrappedConfidence(confidence.value)), originNode, r.add.endorsement, relayId, r.relayFeeMsat)
77+
case None =>
78+
val confidence = (r.add.endorsement + 0.5) / 8
79+
context.self ! WrappedConfidence(confidence)
80+
}
7481
Behaviors.receiveMessagePartial {
7582
case WrappedConfidence(confidence) =>
76-
new ChannelRelay(nodeParams, register, reputationRecorder, channels, r, upstream, confidence, context, relayId).relay(Seq.empty)
83+
context.self ! DoRelay
84+
new ChannelRelay(nodeParams, register, reputationRecorder_opt, channels, r, upstream, confidence, context, relayId).relay(Seq.empty)
7785
}
7886
}
7987
}
@@ -115,7 +123,7 @@ object ChannelRelay {
115123
*/
116124
class ChannelRelay private(nodeParams: NodeParams,
117125
register: ActorRef,
118-
reputationRecorder: typed.ActorRef[ReputationRecorder.StandardCommand],
126+
reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.ChannelRelayCommand]],
119127
channels: Map[ByteVector32, Relayer.OutgoingChannel],
120128
r: IncomingPaymentPacket.ChannelRelayPacket,
121129
upstream: Upstream.Hot.Channel,
@@ -131,6 +139,8 @@ class ChannelRelay private(nodeParams: NodeParams,
131139
private case class PreviouslyTried(channelId: ByteVector32, failure: RES_ADD_FAILED[ChannelException])
132140

133141
def relay(previousFailures: Seq[PreviouslyTried]): Behavior[Command] = {
142+
Behaviors.receiveMessagePartial {
143+
case DoRelay =>
134144
if (previousFailures.isEmpty) {
135145
context.log.info("relaying htlc #{} from channelId={} to requestedShortChannelId={} nextNode={}", r.add.id, r.add.channelId, r.payload.outgoingChannelId, nextNodeId_opt.getOrElse(""))
136146
}
@@ -139,13 +149,14 @@ class ChannelRelay private(nodeParams: NodeParams,
139149
case RelayFailure(cmdFail) =>
140150
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
141151
context.log.info("rejecting htlc reason={}", cmdFail.reason)
142-
reputationRecorder ! CancelRelay(upstream.receivedFrom, r.add.endorsement, relayId)
152+
reputationRecorder_opt.foreach(_ ! CancelRelay(upstream.receivedFrom, r.add.endorsement, relayId))
143153
safeSendAndStop(r.add.channelId, cmdFail)
144154
case RelaySuccess(selectedChannelId, cmdAdd) =>
145155
context.log.info("forwarding htlc #{} from channelId={} to channelId={}", r.add.id, r.add.channelId, selectedChannelId)
146156
register ! Register.Forward(forwardFailureAdapter, selectedChannelId, cmdAdd)
147157
waitForAddResponse(selectedChannelId, previousFailures)
148158
}
159+
}
149160
}
150161

151162
def waitForAddResponse(selectedChannelId: ByteVector32, previousFailures: Seq[PreviouslyTried]): Behavior[Command] =
@@ -154,11 +165,12 @@ class ChannelRelay private(nodeParams: NodeParams,
154165
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${upstream.add.id}")
155166
val cmdFail = CMD_FAIL_HTLC(upstream.add.id, Right(UnknownNextPeer()), commit = true)
156167
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
157-
reputationRecorder ! CancelRelay(upstream.receivedFrom, r.add.endorsement, relayId)
168+
reputationRecorder_opt.foreach(_ ! CancelRelay(upstream.receivedFrom, r.add.endorsement, relayId))
158169
safeSendAndStop(upstream.add.channelId, cmdFail)
159170

160171
case WrappedAddResponse(addFailed: RES_ADD_FAILED[_]) =>
161172
context.log.info("attempt failed with reason={}", addFailed.t.getClass.getSimpleName)
173+
context.self ! DoRelay
162174
relay(previousFailures :+ PreviouslyTried(selectedChannelId, addFailed))
163175

164176
case WrappedAddResponse(r: RES_SUCCESS[_]) =>
@@ -331,7 +343,7 @@ class ChannelRelay private(nodeParams: NodeParams,
331343
}
332344

333345
private def recordRelayDuration(isSuccess: Boolean): Unit = {
334-
reputationRecorder ! RecordResult(upstream.receivedFrom, r.add.endorsement, relayId, isSuccess)
346+
reputationRecorder_opt.foreach(_ ! RecordResult(upstream.receivedFrom, r.add.endorsement, relayId, isSuccess))
335347
Metrics.RelayedPaymentDuration
336348
.withTag(Tags.Relay, Tags.RelayType.Channel)
337349
.withTag(Tags.Success, isSuccess)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ object ChannelRelayer {
5959

6060
def apply(nodeParams: NodeParams,
6161
register: ActorRef,
62-
reputationRecorder: typed.ActorRef[ReputationRecorder.StandardCommand],
62+
reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.ChannelRelayCommand]],
6363
channels: Map[ByteVector32, Relayer.OutgoingChannel] = Map.empty,
6464
scid2channels: Map[ShortChannelId, ByteVector32] = Map.empty,
6565
node2channels: mutable.MultiDict[PublicKey, ByteVector32] = mutable.MultiDict.empty): Behavior[Command] =
@@ -81,7 +81,7 @@ object ChannelRelayer {
8181
case None => Map.empty
8282
}
8383
context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), nextChannels.keys.mkString(","))
84-
context.spawn(ChannelRelay.apply(nodeParams, register, reputationRecorder, nextChannels, originNode, relayId, channelRelayPacket), name = relayId.toString)
84+
context.spawn(ChannelRelay.apply(nodeParams, register, reputationRecorder_opt, nextChannels, originNode, relayId, channelRelayPacket), name = relayId.toString)
8585
Behaviors.same
8686

8787
case GetOutgoingChannels(replyTo, Relayer.GetOutgoingChannels(enabledOnly)) =>
@@ -102,14 +102,14 @@ object ChannelRelayer {
102102
context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId)
103103
val scid2channels1 = scid2channels ++ mappings
104104
val node2channels1 = node2channels.addOne(remoteNodeId, channelId)
105-
apply(nodeParams, register, reputationRecorder, channels1, scid2channels1, node2channels1)
105+
apply(nodeParams, register, reputationRecorder_opt, channels1, scid2channels1, node2channels1)
106106

107107
case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortIds, remoteNodeId)) =>
108108
context.log.debug(s"removed local channel info for channelId=$channelId localAlias=${shortIds.localAlias}")
109109
val channels1 = channels - channelId
110110
val scid2Channels1 = scid2channels - shortIds.localAlias -- shortIds.real.toOption
111111
val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId)
112-
apply(nodeParams, register, reputationRecorder, channels1, scid2Channels1, node2channels1)
112+
apply(nodeParams, register, reputationRecorder_opt, channels1, scid2Channels1, node2channels1)
113113

114114
case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortIds, commitments)) =>
115115
val channels1 = channels.get(channelId) match {
@@ -118,7 +118,7 @@ object ChannelRelayer {
118118
channels + (channelId -> c.copy(commitments = commitments))
119119
case None => channels // we only consider the balance if we have the channel_update
120120
}
121-
apply(nodeParams, register, reputationRecorder, channels1, scid2channels, node2channels)
121+
apply(nodeParams, register, reputationRecorder_opt, channels1, scid2channels, node2channels)
122122

123123
}
124124
}

0 commit comments

Comments
 (0)