Skip to content

Commit 96b5205

Browse files
committed
Bidirectional reputation
1 parent a2fa398 commit 96b5205

49 files changed

Lines changed: 677 additions & 636 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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
@@ -217,6 +217,7 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef,
217217
onion: OnionRoutingPacket,
218218
nextPathKey_opt: Option[PublicKey],
219219
confidence: Double,
220+
endorsement: Int,
220221
fundingFee_opt: Option[LiquidityAds.FundingFee],
221222
origin: Origin.Hot,
222223
commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
@@ -258,7 +259,7 @@ final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToComm
258259
final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand
259260
final case class CMD_GET_CHANNEL_INFO(replyTo: akka.actor.typed.ActorRef[RES_GET_CHANNEL_INFO]) extends Command
260261

261-
case class OutgoingHtlcAdded(add: UpdateAddHtlc, upstream: Upstream.Hot, fee: MilliSatoshi)
262+
case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, upstream: Upstream.Hot, fee: MilliSatoshi)
262263
case class OutgoingHtlcFailed(fail: HtlcFailureMessage)
263264
case class OutgoingHtlcFulfilled(fulfill: UpdateFulfillHtlc)
264265

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
@@ -912,7 +912,7 @@ case class Commitments(channelParams: ChannelParams,
912912
return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = cmd.amount))
913913
}
914914

915-
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.confidence, cmd.fundingFee_opt)
915+
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.endorsement, cmd.fundingFee_opt)
916916
// we increment the local htlc index and add an entry to the origins map
917917
val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1)
918918
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
@@ -514,7 +514,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
514514
case Right((commitments1, add)) =>
515515
if (c.commit) self ! CMD_SIGN()
516516
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt))
517-
context.system.eventStream.publish(OutgoingHtlcAdded(add, c.origin.upstream, nodeFee(d.channelUpdate.relayFees, add.amountMsat)))
517+
context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, c.origin.upstream, nodeFee(d.channelUpdate.relayFees, add.amountMsat)))
518518
handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add
519519
case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate))
520520
}

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: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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,18 @@ 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+
channels.values.headOption.map(_.nextNodeId) match {
83+
case Some(nextNodeId) =>
84+
reputationRecorder ! GetConfidence(context.messageAdapter[ReputationRecorder.Confidence](confidence => WrappedConfidence(confidence.confidence, confidence.endorsement)), upstream, nextNodeId, r.relayFeeMsat)
85+
case None =>
86+
context.self ! WrappedConfidence(0.0, 0)
87+
}
8388
case None =>
84-
val confidence = (r.add.endorsement + 0.5) / 8
85-
context.self ! WrappedConfidence(confidence)
89+
context.self ! WrappedConfidence(1.0, r.add.endorsement)
8690
}
8791
Behaviors.receiveMessagePartial {
88-
case WrappedConfidence(confidence) =>
89-
new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, context).start()
92+
case WrappedConfidence(confidence, endorsement) =>
93+
new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, endorsement, context).start()
9094
}
9195
}
9296
}
@@ -133,6 +137,7 @@ class ChannelRelay private(nodeParams: NodeParams,
133137
r: IncomingPaymentPacket.ChannelRelayPacket,
134138
upstream: Upstream.Hot.Channel,
135139
confidence: Double,
140+
endorsement: Int,
136141
context: ActorContext[ChannelRelay.Command]) {
137142

138143
import ChannelRelay._
@@ -412,7 +417,7 @@ class ChannelRelay private(nodeParams: NodeParams,
412417
RelayFailure(makeCmdFailHtlc(r.add.id, ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))))
413418
case None =>
414419
val origin = Origin.Hot(addResponseAdapter.toClassic, upstream)
415-
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))
420+
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))
416421
}
417422
}
418423

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._
@@ -71,29 +70,27 @@ object NodeRelay {
7170
private case class WrappedResolvedPaths(resolved: Seq[ResolvedPath]) extends Command
7271
private case class WrappedPeerInfo(remoteFeatures_opt: Option[Features[InitFeature]]) extends Command
7372
private case class WrappedOnTheFlyFundingResponse(result: Peer.ProposeOnTheFlyFundingResponse) extends Command
74-
private case class WrappedConfidence(confidence: Double) extends Command
7573
// @formatter:on
7674

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

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

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

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

376362
/**
@@ -435,7 +421,7 @@ class NodeRelay private(nodeParams: NodeParams,
435421
case r: BlindedRecipient => r.blindedHops.headOption
436422
}
437423
val dummyRoute = Route(amountOut, Seq(dummyHop), finalHop_opt)
438-
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, 1.0) match {
424+
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, 1.0, Reputation.maxEndorsement) match {
439425
case Left(f) =>
440426
context.log.warn("could not create payment onion for on-the-fly funding: {}", f.getMessage)
441427
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

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2727
import fr.acinq.eclair.channel._
2828
import fr.acinq.eclair.crypto.Sphinx
2929
import fr.acinq.eclair.payment.Monitoring.Metrics
30+
import fr.acinq.eclair.reputation.Reputation
3031
import fr.acinq.eclair.wire.protocol.LiquidityAds.PaymentDetails
3132
import fr.acinq.eclair.wire.protocol._
3233
import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, ToMilliSatoshiConversion}
@@ -309,7 +310,7 @@ object OnTheFlyFunding {
309310
// This lets us detect that this HTLC is an on-the-fly funded HTLC.
310311
val htlcFees = LiquidityAds.FundingFee(remainingFees.min(p.maxFees(htlcMinimum)), cmd.status.txId)
311312
val origin = Origin.Hot(htlcSettledAdapter.toClassic, p.upstream)
312-
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.pathKey_opt, 1.0, Some(htlcFees), origin, commit = true)
313+
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.pathKey_opt, 1.0, Reputation.maxEndorsement, Some(htlcFees), origin, commit = true)
313314
cmd.channel ! add
314315
remainingFees - htlcFees.amount
315316
}

0 commit comments

Comments
 (0)