Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ eclair {
// Payments that stay pending for longer than this get penalized.
max-relay-duration = 5 minutes
}

reserved-for-accountable = 0.5 // Fraction of a channel slots and usable liquidity that is reserved for accountable HTLCs
}

on-chain-fees {
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = {
getRouteParams(pathFindingExperimentName_opt) match {
case Right(routeParams) =>
val target = ClearRecipient(targetNodeId, Features.empty, amount, CltvExpiry(appKit.nodeParams.currentBlockHeight), ByteVector32.Zeroes, extraEdges)
val target = ClearRecipient(targetNodeId, Features.empty, amount, CltvExpiry(appKit.nodeParams.currentBlockHeight), ByteVector32.Zeroes, extraEdges, upgradeAccountability = true)
val routeParams1 = routeParams.copy(
includeLocalChannelCost = includeLocalChannelCost,
boundaries = routeParams.boundaries.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ object NodeParams extends Logging {
halfLife = FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS),
maxRelayDuration = FiniteDuration(config.getDuration("relay.peer-reputation.max-relay-duration").getSeconds, TimeUnit.SECONDS),
),
reservedBucket = config.getDouble("relay.reserved-for-accountable"),
),
db = database,
autoReconnect = config.getBoolean("auto-reconnect"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,12 @@ object Upstream {
override val amountIn: MilliSatoshi = add.amountMsat
val expiryIn: CltvExpiry = add.cltvExpiry

override def toString: String = s"Channel(amountIn=$amountIn, receivedAt=${receivedAt.toLong}, receivedFrom=${receivedFrom.toHex}, endorsement=${add.endorsement}, incomingChannelOccupancy=$incomingChannelOccupancy)"
override def toString: String = s"Channel(amountIn=$amountIn, receivedAt=${receivedAt.toLong}, receivedFrom=${receivedFrom.toHex}, accountable=${add.accountable}, incomingChannelOccupancy=$incomingChannelOccupancy)"
}
/** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */
case class Trampoline(received: List[Channel]) extends Hot {
override val amountIn: MilliSatoshi = received.map(_.add.amountMsat).sum
val accountable: Boolean = received.map(_.add.accountable).reduce(_ || _)
// We must use the lowest expiry of the incoming HTLC set.
val expiryIn: CltvExpiry = received.map(_.add.cltvExpiry).min
val receivedAt: TimestampMilli = received.map(_.receivedAt).max
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, chan

case class ChannelClosed(channel: ActorRef, channelId: ByteVector32, closingType: ClosingType, commitments: Commitments) extends ChannelEvent

case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, upstream: Upstream.Hot, fee: MilliSatoshi)
case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, fee: MilliSatoshi)

sealed trait OutgoingHtlcSettled

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ case class ForbiddenDuringSplice (override val channelId: Byte
case class ForbiddenDuringQuiescence (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while quiescent")
case class ConcurrentRemoteSplice (override val channelId: ByteVector32) extends ChannelException(channelId, "splice attempt canceled, remote initiated splice before us")
case class TooManySmallHtlcs (override val channelId: ByteVector32, number: Long, below: MilliSatoshi) extends ChannelJammingException(channelId, s"too many small htlcs: $number HTLCs below $below")
case class IncomingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"incoming confidence too low: confidence=$confidence occupancy=$occupancy")
case class OutgoingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"outgoing confidence too low: confidence=$confidence occupancy=$occupancy")
case class MissingCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is missing")
case class InvalidCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is not valid")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,7 @@ case class Commitment(fundingTxIndex: Long,
return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterAdd))
}

// Jamming protection
// Must be the last checks so that they can be ignored for shadow deployment.
reputationScore.checkOutgoingChannelOccupancy(params.channelId, this, outgoingHtlcs.toSeq)
Right(())
}

def canReceiveAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges): Either[ChannelException, Unit] = {
Expand Down Expand Up @@ -898,7 +896,7 @@ case class Commitments(channelParams: ChannelParams,
return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = cmd.amount))
}

val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.reputationScore.endorsement, cmd.fundingFee_opt)
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.reputationScore.accountable, cmd.fundingFee_opt)
// we increment the local htlc index and add an entry to the origins map
val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1)
val originChannels1 = originChannels + (add.id -> cmd.origin)
Expand All @@ -915,7 +913,6 @@ case class Commitments(channelParams: ChannelParams,
Metrics.dropHtlc(failure, Tags.Directions.Outgoing)
failure match {
case f: TooManySmallHtlcs => log.info("TooManySmallHtlcs: {} outgoing HTLCs are below {}", f.number, f.below)
case f: IncomingConfidenceTooLow => log.info("IncomingConfidenceTooLow: confidence is {}% while channel is {}% full", (100 * f.confidence).toInt, (100 * f.occupancy).toInt)
case f: OutgoingConfidenceTooLow => log.info("OutgoingConfidenceTooLow: confidence is {}% while channel is {}% full", (100 * f.confidence).toInt, (100 * f.occupancy).toInt)
case _ => ()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
if (c.commit) self ! CMD_SIGN()
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt))
val relayFee = nodeFee(d.channelUpdate.relayFees, add.amountMsat)
context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, c.origin.upstream, relayFee))
log.info("OutgoingHtlcAdded: channelId={}, id={}, endorsement={}, remoteNodeId={}, upstream={}, fee={}, now={}, blockHeight={}, expiry={}", Array(add.channelId.toHex, add.id, add.endorsement, remoteNodeId.toHex, c.origin.upstream.toString, relayFee, TimestampMilli.now().toLong, nodeParams.currentBlockHeight.toLong, add.cltvExpiry))
context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, relayFee))
log.info("OutgoingHtlcAdded: channelId={}, id={}, accountable={}, remoteNodeId={}, upstream={}, fee={}, now={}, blockHeight={}, expiry={}", Array(add.channelId.toHex, add.id, add.accountable, remoteNodeId.toHex, c.origin.upstream.toString, relayFee, TimestampMilli.now().toLong, nodeParams.currentBlockHeight.toLong, add.cltvExpiry))
handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add
case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat

override lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty)

override lazy val accountable: Boolean = tags.contains(Accountable)

/**
* @return the hash of this payment invoice
*/
Expand Down Expand Up @@ -147,6 +149,7 @@ object Bolt11Invoice {
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
Some(Accountable),
// We want to keep invoices as small as possible, so we explicitly remove unknown features.
Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped()))
).flatten
Expand Down Expand Up @@ -196,7 +199,7 @@ object Bolt11Invoice {
case class UnknownTag28(data: BitVector) extends UnknownTaggedField
case class UnknownTag29(data: BitVector) extends UnknownTaggedField
case class UnknownTag30(data: BitVector) extends UnknownTaggedField
case class UnknownTag31(data: BitVector) extends UnknownTaggedField
case class InvalidTag31(data: BitVector) extends InvalidTaggedField
// @formatter:on

/**
Expand Down Expand Up @@ -283,6 +286,12 @@ object Bolt11Invoice {
}
}

/**
* Present if the recipient is willing to be held accountable for the timely resolution of HTLCs.
*/
case object Accountable extends TaggedField


/**
* This returns a bitvector with the minimum size necessary to encode the long, left padded to have a length (in bits)
* that is a multiple of 5.
Expand Down Expand Up @@ -439,7 +448,10 @@ object Bolt11Invoice {
.typecase(28, dataCodec(bits).as[UnknownTag28])
.typecase(29, dataCodec(bits).as[UnknownTag29])
.typecase(30, dataCodec(bits).as[UnknownTag30])
.typecase(31, dataCodec(bits).as[UnknownTag31])
.\(31) {
case Accountable => Accountable
case a: InvalidTag31 => a: TaggedField
}(choice(dataCodec(provide(Accountable), expectedLength = Some(0)).upcast[TaggedField], dataCodec(bits).as[InvalidTag31].upcast[TaggedField]))

private def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A](
(data: A) => codec.encode(data),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {
val blindedPaths: Seq[PaymentBlindedRoute] = records.get[InvoicePaths].get.paths.zip(records.get[InvoiceBlindedPay].get.paymentInfo).map { case (route, info) => PaymentBlindedRoute(route, info) }
val fallbacks: Option[Seq[FallbackAddress]] = records.get[InvoiceFallbacks].map(_.addresses)
val signature: ByteVector64 = records.get[Signature].get.signature
override val accountable: Boolean = records.records.contains(InvoiceAccountable)

// It is assumed that the request is valid for this offer.
def validateFor(request: InvoiceRequest, pathNodeId: PublicKey): Either[String, Unit] = {
Expand Down Expand Up @@ -109,6 +110,7 @@ object Bolt12Invoice {
val amount = request.amount
val tlvs: Set[InvoiceTlv] = removeSignature(request.records).records ++ Set(
Some(InvoicePaths(paths.map(_.route))),
Some(InvoiceAccountable),
Some(InvoiceBlindedPay(paths.map(_.paymentInfo))),
Some(InvoiceCreatedAt(TimestampSecond.now())),
Some(InvoiceRelativeExpiry(invoiceExpiry.toSeconds)),
Expand Down Expand Up @@ -168,6 +170,7 @@ case class MinimalBolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice
override val createdAt: TimestampSecond = records.get[InvoiceCreatedAt].get.timestamp
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[InvoiceRelativeExpiry].map(_.seconds).getOrElse(Bolt12Invoice.DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
override val features: Features[InvoiceFeature] = records.get[InvoiceFeatures].map(f => Features(f.features).invoiceFeatures()).getOrElse(Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional))
override def accountable: Boolean = true

override def toString: String = {
val data = OfferCodecs.invoiceTlvCodec.encode(records).require.bytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ trait Invoice {
def features: Features[InvoiceFeature]
def isExpired(now: TimestampSecond = TimestampSecond.now()): Boolean = createdAt + relativeExpiry.toSeconds <= now
def toString: String
def accountable: Boolean
// @formatter:on
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ object Monitoring {
val WakeUp = "WakeUp"
val Remote = "Remote"
val Malformed = "MalformedHtlc"
val Jamming = "Jamming"

def apply(cmdFail: CMD_FAIL_HTLC): String = cmdFail.reason match {
case _: FailureReason.EncryptedDownstreamFailure => Remote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.send.Recipient
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.Router.Route
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{InvoiceRoutingInfo, OutgoingBlindedPaths}
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{InvoiceRoutingInfo, OutgoingBlindedPaths, UpgradeAccountability}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomBytes32, randomKey}
Expand Down Expand Up @@ -144,6 +144,8 @@ object IncomingPaymentPacket {
}
}
case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case None if add.accountable && !payload.records.contains(UpgradeAccountability) =>
Left(InvalidOnionPayload(UInt64(19), 0))
case None =>
// We are not inside a blinded path: channel relay information is directly available.
IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now()))
Expand All @@ -160,6 +162,8 @@ object IncomingPaymentPacket {
case DecodedEncryptedRecipientData(blindedPayload, _) => validateBlindedFinalPayload(add, payload, blindedPayload)
}
case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case None if add.accountable && !payload.records.contains(UpgradeAccountability) =>
Left(InvalidOnionPayload(UInt64(19), 0))
case None =>
// We check if the payment is using trampoline: if it is, we may not be the final recipient.
payload.get[OnionPaymentPayloadTlv.TrampolineOnion] match {
Expand Down Expand Up @@ -219,6 +223,7 @@ object IncomingPaymentPacket {
case payload if add.amountMsat < payload.paymentRelayData.paymentConstraints.minAmount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.cltvExpiry > payload.paymentRelayData.paymentConstraints.maxCltvExpiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if !Features.areCompatible(Features.empty, payload.paymentRelayData.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case _ if add.accountable && !blindedPayload.records.contains(RouteBlindingEncryptedDataTlv.UpgradeAccountability) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload => Right(ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now()))
}
}
Expand All @@ -237,6 +242,7 @@ object IncomingPaymentPacket {
case payload if payload.paymentConstraints_opt.exists(c => c.maxCltvExpiry < add.cltvExpiry) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.cltvExpiry < payload.expiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case _ if add.accountable && !blindedPayload.records.contains(RouteBlindingEncryptedDataTlv.UpgradeAccountability) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload => Right(FinalPacket(add, payload, TimestampMilli.now()))
}
}
Expand All @@ -253,7 +259,7 @@ object IncomingPaymentPacket {
// We merge contents from the outer and inner payloads.
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet)
Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket), TimestampMilli.now()))
Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket, upgradeAccountability = outerPayload.upgradeAccountability), TimestampMilli.now()))
}
}
}
Expand Down
Loading