Skip to content

Commit a8e8ab0

Browse files
committed
Bidirectional reputation
1 parent 0548348 commit a8e8ab0

27 files changed

+198
-142
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef,
209209
onion: OnionRoutingPacket,
210210
nextPathKey_opt: Option[PublicKey],
211211
confidence: Double,
212+
endorsement: Int,
212213
fundingFee_opt: Option[LiquidityAds.FundingFee],
213214
origin: Origin.Hot,
214215
commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
@@ -247,7 +248,7 @@ final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToComm
247248
final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand
248249
final case class CMD_GET_CHANNEL_INFO(replyTo: akka.actor.typed.ActorRef[RES_GET_CHANNEL_INFO]) extends Command
249250

250-
case class OutgoingHtlcAdded(add: UpdateAddHtlc, upstream: Upstream.Hot, fee: MilliSatoshi)
251+
case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, upstream: Upstream.Hot, fee: MilliSatoshi)
251252
case class OutgoingHtlcFailed(fail: HtlcFailureMessage)
252253
case class OutgoingHtlcFulfilled(fulfill: UpdateFulfillHtlc)
253254

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ case class Commitments(params: ChannelParams,
881881
return Left(HtlcValueTooSmall(params.channelId, minimum = htlcMinimum, actual = cmd.amount))
882882
}
883883

884-
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.confidence, cmd.fundingFee_opt)
884+
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.endorsement, cmd.fundingFee_opt)
885885
// we increment the local htlc index and add an entry to the origins map
886886
val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1)
887887
val originChannels1 = originChannels + (add.id -> cmd.origin)

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
465465
case Right((commitments1, add)) =>
466466
if (c.commit) self ! CMD_SIGN()
467467
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt))
468-
context.system.eventStream.publish(OutgoingHtlcAdded(add, c.origin.upstream, nodeFee(d.channelUpdate.relayFees, add.amountMsat)))
468+
context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, c.origin.upstream, nodeFee(d.channelUpdate.relayFees, add.amountMsat)))
469469
handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add
470470
case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate))
471471
}

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,12 +338,12 @@ object OutgoingPaymentPacket {
338338
}
339339

340340
/** Build the command to add an HTLC for the given recipient using the provided route. */
341-
def buildOutgoingPayment(origin: Origin.Hot, paymentHash: ByteVector32, route: Route, recipient: Recipient, confidence: Double): Either[OutgoingPaymentError, OutgoingPaymentPacket] = {
341+
def buildOutgoingPayment(origin: Origin.Hot, paymentHash: ByteVector32, route: Route, recipient: Recipient, confidence: Double, endorsement: Int): Either[OutgoingPaymentError, OutgoingPaymentPacket] = {
342342
for {
343343
payment <- recipient.buildPayloads(paymentHash, route)
344344
onion <- buildOnion(payment.payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)) // BOLT 2 requires that associatedData == paymentHash
345345
} yield {
346-
val cmd = CMD_ADD_HTLC(origin.replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerPathKey_opt, confidence, fundingFee_opt = None, origin, commit = true)
346+
val cmd = CMD_ADD_HTLC(origin.replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerPathKey_opt, confidence, endorsement, fundingFee_opt = None, origin, commit = true)
347347
OutgoingPaymentPacket(cmd, route.hops.head.shortChannelId, onion.sharedSecrets)
348348
}
349349
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import fr.acinq.eclair.io.{Peer, PeerReadyNotifier}
3232
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
3333
import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams}
3434
import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket}
35-
import fr.acinq.eclair.reputation.ReputationRecorder
35+
import fr.acinq.eclair.reputation.{ConfidenceInt, ReputationRecorder}
3636
import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence
3737
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.createBadOnionFailure
3838
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
@@ -50,7 +50,7 @@ object ChannelRelay {
5050
sealed trait Command
5151
private case object DoRelay extends Command
5252
private case class WrappedPeerReadyResult(result: PeerReadyNotifier.Result) extends Command
53-
private case class WrappedConfidence(confidence: Double) extends Command
53+
private case class WrappedConfidence(confidence: Double, endorsement: Int) extends Command
5454
private case class WrappedForwardFailure(failure: Register.ForwardFailure[CMD_ADD_HTLC]) extends Command
5555
private case class WrappedAddResponse(res: CommandResponse[CMD_ADD_HTLC]) extends Command
5656
private case class WrappedOnTheFlyFundingResponse(result: Peer.ProposeOnTheFlyFundingResponse) extends Command
@@ -79,14 +79,14 @@ object ChannelRelay {
7979
val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), r.receivedAt, originNode)
8080
reputationRecorder_opt match {
8181
case Some(reputationRecorder) =>
82-
reputationRecorder ! GetConfidence(context.messageAdapter[ReputationRecorder.Confidence](confidence => WrappedConfidence(confidence.value)), upstream, r.relayFeeMsat)
82+
val nextNodeId_opt = channels.values.headOption.map(_.nextNodeId)
83+
reputationRecorder ! GetConfidence(context.messageAdapter[ReputationRecorder.Confidence](confidence => WrappedConfidence(confidence.value, confidence.endorsement)), upstream, nextNodeId_opt, r.relayFeeMsat)
8384
case None =>
84-
val confidence = (r.add.endorsement + 0.5) / 8
85-
context.self ! WrappedConfidence(confidence)
85+
context.self ! WrappedConfidence(1.0, r.add.endorsement)
8686
}
8787
Behaviors.receiveMessagePartial {
88-
case WrappedConfidence(confidence) =>
89-
new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, context).start()
88+
case WrappedConfidence(confidence, endorsement) =>
89+
new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, endorsement, context).start()
9090
}
9191
}
9292
}
@@ -132,6 +132,7 @@ class ChannelRelay private(nodeParams: NodeParams,
132132
r: IncomingPaymentPacket.ChannelRelayPacket,
133133
upstream: Upstream.Hot.Channel,
134134
confidence: Double,
135+
endorsement: Int,
135136
context: ActorContext[ChannelRelay.Command]) {
136137

137138
import ChannelRelay._
@@ -406,7 +407,7 @@ class ChannelRelay private(nodeParams: NodeParams,
406407
RelayFailure(makeCmdFailHtlc(r.add.id, ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))))
407408
case None =>
408409
val origin = Origin.Hot(addResponseAdapter.toClassic, upstream)
409-
RelaySuccess(outgoingChannel.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, r.amountToForward, r.add.paymentHash, r.outgoingCltv, r.nextPacket, nextPathKey_opt, confidence, fundingFee_opt = None, origin, commit = true))
410+
RelaySuccess(outgoingChannel.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, r.amountToForward, r.add.paymentHash, r.outgoingCltv, r.nextPacket, nextPathKey_opt, confidence, endorsement, fundingFee_opt = None, origin, commit = true))
410411
}
411412
}
412413

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

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived,
3737
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
3838
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode
3939
import fr.acinq.eclair.payment.send._
40+
import fr.acinq.eclair.reputation.Reputation
41+
import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence
4042
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RouteParams}
41-
import fr.acinq.eclair.reputation.ReputationRecorder
42-
import fr.acinq.eclair.reputation.ReputationRecorder.GetTrampolineConfidence
43-
import fr.acinq.eclair.router.Router.RouteParams
4443
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
4544
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
4645
import fr.acinq.eclair.wire.protocol._
@@ -70,29 +69,27 @@ object NodeRelay {
7069
private case class WrappedResolvedPaths(resolved: Seq[ResolvedPath]) extends Command
7170
private case class WrappedPeerInfo(remoteFeatures_opt: Option[Features[InitFeature]]) extends Command
7271
private case class WrappedOnTheFlyFundingResponse(result: Peer.ProposeOnTheFlyFundingResponse) extends Command
73-
private case class WrappedConfidence(confidence: Double) extends Command
7472
// @formatter:on
7573

7674
trait OutgoingPaymentFactory {
7775
def spawnOutgoingPayFSM(context: ActorContext[NodeRelay.Command], cfg: SendPaymentConfig, multiPart: Boolean): ActorRef
7876
}
7977

80-
case class SimpleOutgoingPaymentFactory(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends OutgoingPaymentFactory {
78+
case class SimpleOutgoingPaymentFactory(nodeParams: NodeParams, router: ActorRef, register: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[GetConfidence]]) extends OutgoingPaymentFactory {
8179
val paymentFactory: PaymentInitiator.SimplePaymentFactory = PaymentInitiator.SimplePaymentFactory(nodeParams, router, register)
8280

8381
override def spawnOutgoingPayFSM(context: ActorContext[Command], cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
8482
if (multiPart) {
8583
context.toClassic.actorOf(MultiPartPaymentLifecycle.props(nodeParams, cfg, publishPreimage = true, router, paymentFactory))
8684
} else {
87-
context.toClassic.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register))
85+
context.toClassic.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register, reputationRecorder_opt))
8886
}
8987
}
9088
}
9189

9290
def apply(nodeParams: NodeParams,
9391
parent: typed.ActorRef[NodeRelayer.Command],
9492
register: ActorRef,
95-
reputationRecorder_opt: Option[typed.ActorRef[GetTrampolineConfidence]],
9693
relayId: UUID,
9794
nodeRelayPacket: NodeRelayPacket,
9895
outgoingPaymentFactory: OutgoingPaymentFactory,
@@ -116,7 +113,7 @@ object NodeRelay {
116113
case _: IncomingPaymentPacket.RelayToNonTrampolinePacket => None
117114
case _: IncomingPaymentPacket.RelayToBlindedPathsPacket => None
118115
}
119-
new NodeRelay(nodeParams, parent, register, reputationRecorder_opt, relayId, paymentHash, nodeRelayPacket.outerPayload.paymentSecret, context, outgoingPaymentFactory, router)
116+
new NodeRelay(nodeParams, parent, register, relayId, paymentHash, nodeRelayPacket.outerPayload.paymentSecret, context, outgoingPaymentFactory, router)
120117
.receiving(Queue.empty, nodeRelayPacket.innerPayload, nextPacket_opt, incomingPaymentHandler)
121118
}
122119
}
@@ -205,7 +202,6 @@ object NodeRelay {
205202
class NodeRelay private(nodeParams: NodeParams,
206203
parent: akka.actor.typed.ActorRef[NodeRelayer.Command],
207204
register: ActorRef,
208-
reputationRecorder_opt: Option[typed.ActorRef[GetTrampolineConfidence]],
209205
relayId: UUID,
210206
paymentHash: ByteVector32,
211207
paymentSecret: ByteVector32,
@@ -343,33 +339,23 @@ class NodeRelay private(nodeParams: NodeParams,
343339
context.log.debug("relaying trampoline payment (amountIn={} expiryIn={} amountOut={} expiryOut={} isWallet={})", upstream.amountIn, upstream.expiryIn, amountOut, expiryOut, walletNodeId_opt.isDefined)
344340
// We only make one try when it's a direct payment to a wallet.
345341
val maxPaymentAttempts = if (walletNodeId_opt.isDefined) 1 else nodeParams.maxPaymentAttempts
346-
val totalFee = upstream.amountIn - payloadOut.outgoingAmount(upstream.amountIn)
347-
reputationRecorder_opt match {
348-
case Some(reputationRecorder) => reputationRecorder ! GetTrampolineConfidence(context.messageAdapter(confidence => WrappedConfidence(confidence.value)), upstream, totalFee)
349-
case None => context.self ! WrappedConfidence((upstream.received.map(_.add.endorsement).min + 0.5) / 8)
350-
}
351-
Behaviors.receiveMessagePartial {
352-
rejectExtraHtlcPartialFunction orElse {
353-
case WrappedConfidence(confidence) =>
354-
val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, confidence)
355-
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, amountOut, expiryOut)
356-
// If the next node is using trampoline, we assume that they support MPP.
357-
val useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment) || packetOut_opt.nonEmpty
358-
val payFsmAdapters = {
359-
context.messageAdapter[PreimageReceived](WrappedPreimageReceived)
360-
context.messageAdapter[PaymentSent](WrappedPaymentSent)
361-
context.messageAdapter[PaymentFailed](WrappedPaymentFailed)
362-
}.toClassic
363-
val payment = if (useMultiPart) {
364-
SendMultiPartPayment(payFsmAdapters, recipient, maxPaymentAttempts, routeParams)
365-
} else {
366-
SendPaymentToNode(payFsmAdapters, recipient, maxPaymentAttempts, routeParams)
367-
}
368-
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, useMultiPart)
369-
payFSM ! payment
370-
sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, payloadOut, TimestampMilli.now(), fulfilledUpstream = false)
371-
}
342+
val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true)
343+
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, amountOut, expiryOut)
344+
// If the next node is using trampoline, we assume that they support MPP.
345+
val useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment) || packetOut_opt.nonEmpty
346+
val payFsmAdapters = {
347+
context.messageAdapter[PreimageReceived](WrappedPreimageReceived)
348+
context.messageAdapter[PaymentSent](WrappedPaymentSent)
349+
context.messageAdapter[PaymentFailed](WrappedPaymentFailed)
350+
}.toClassic
351+
val payment = if (useMultiPart) {
352+
SendMultiPartPayment(payFsmAdapters, recipient, maxPaymentAttempts, routeParams)
353+
} else {
354+
SendPaymentToNode(payFsmAdapters, recipient, maxPaymentAttempts, routeParams)
372355
}
356+
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, useMultiPart)
357+
payFSM ! payment
358+
sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, payloadOut, TimestampMilli.now(), fulfilledUpstream = false)
373359
}
374360

375361
/**
@@ -434,7 +420,7 @@ class NodeRelay private(nodeParams: NodeParams,
434420
case r: BlindedRecipient => r.blindedHops.headOption
435421
}
436422
val dummyRoute = Route(amountOut, Seq(dummyHop), finalHop_opt)
437-
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, 1.0) match {
423+
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, 1.0, Reputation.maxEndorsement) match {
438424
case Left(f) =>
439425
context.log.warn("could not create payment onion for on-the-fly funding: {}", f.getMessage)
440426
rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import akka.actor.typed.{ActorRef, Behavior}
2121
import fr.acinq.bitcoin.scalacompat.ByteVector32
2222
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2323
import fr.acinq.eclair.payment._
24-
import fr.acinq.eclair.reputation.ReputationRecorder
2524
import fr.acinq.eclair.{Logs, NodeParams}
2625

2726
import java.util.UUID
@@ -58,12 +57,7 @@ object NodeRelayer {
5857
* NB: the payment secret used here is different from the invoice's payment secret and ensures we can
5958
* group together HTLCs that the previous trampoline node sent in the same MPP.
6059
*/
61-
def apply(nodeParams: NodeParams,
62-
register: akka.actor.ActorRef,
63-
reputationRecorder_opt: Option[ActorRef[ReputationRecorder.GetTrampolineConfidence]],
64-
outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory,
65-
router: akka.actor.ActorRef,
66-
children: Map[PaymentKey, ActorRef[NodeRelay.Command]] = Map.empty): Behavior[Command] =
60+
def apply(nodeParams: NodeParams, register: akka.actor.ActorRef, outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory, router: akka.actor.ActorRef, children: Map[PaymentKey, ActorRef[NodeRelay.Command]] = Map.empty): Behavior[Command] =
6761
Behaviors.setup { context =>
6862
Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) {
6963
Behaviors.receiveMessage {
@@ -78,15 +72,15 @@ object NodeRelayer {
7872
case None =>
7973
val relayId = UUID.randomUUID()
8074
context.log.debug(s"spawning a new handler with relayId=$relayId")
81-
val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, reputationRecorder_opt, relayId, nodeRelayPacket, outgoingPaymentFactory, router), relayId.toString)
75+
val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, outgoingPaymentFactory, router), relayId.toString)
8276
context.log.debug("forwarding incoming htlc #{} from channel {} to new handler", htlcIn.id, htlcIn.channelId)
8377
handler ! NodeRelay.Relay(nodeRelayPacket, originNode)
84-
apply(nodeParams, register, reputationRecorder_opt, outgoingPaymentFactory, router, children + (childKey -> handler))
78+
apply(nodeParams, register, outgoingPaymentFactory, router, children + (childKey -> handler))
8579
}
8680
case RelayComplete(childHandler, paymentHash, paymentSecret) =>
8781
// we do a back-and-forth between parent and child before stopping the child to prevent a race condition
8882
childHandler ! NodeRelay.Stop
89-
apply(nodeParams, register, reputationRecorder_opt, outgoingPaymentFactory, router, children - PaymentKey(paymentHash, paymentSecret))
83+
apply(nodeParams, register, outgoingPaymentFactory, router, children - PaymentKey(paymentHash, paymentSecret))
9084
case GetPendingPayments(replyTo) =>
9185
replyTo ! children
9286
Behaviors.same

0 commit comments

Comments
 (0)