From 8ebf7a453ae9d987632697c60e1ae81f1c6783f9 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 14 Aug 2023 14:50:28 +0200 Subject: [PATCH] Add feature --- eclair-core/src/main/resources/reference.conf | 1 + .../src/main/scala/fr/acinq/eclair/Features.scala | 6 ++++++ .../scala/fr/acinq/eclair/channel/fsm/Channel.scala | 9 ++++++--- .../scala/fr/acinq/eclair/payment/PaymentPacket.scala | 7 ++++++- .../eclair/payment/relay/PostRestartHtlcCleaner.scala | 10 ++++++---- .../scala/fr/acinq/eclair/payment/relay/Relayer.scala | 8 +++++--- .../fr/acinq/eclair/wire/protocol/PaymentOnion.scala | 2 ++ 7 files changed, 32 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 6b1622185b..a1d594efb7 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -69,6 +69,7 @@ eclair { option_route_blinding = disabled option_shutdown_anysegwit = optional option_dual_fund = disabled + option_attributable_error = optional option_quiesce = disabled option_onion_messages = optional option_channel_type = optional diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 20db6a6c87..1159ba3076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -247,6 +247,11 @@ object Features { val mandatory = 28 } + case object AttributableError extends Feature with InitFeature with NodeFeature with Bolt11Feature { + val rfcName = "option_attributable_error" + val mandatory = 30 + } + // TODO: this should also extend NodeFeature once the spec is finalized case object Quiescence extends Feature with InitFeature { val rfcName = "option_quiesce" @@ -322,6 +327,7 @@ object Features { RouteBlinding, ShutdownAnySegwit, DualFunding, + AttributableError, Quiescence, OnionMessages, ChannelType, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index bc110c8128..6de14c70e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -624,7 +624,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case PostRevocationAction.RejectHtlc(add) => log.debug("rejecting incoming htlc {}", add) // NB: we don't set commit = true, we will sign all updates at once afterwards. - self ! CMD_FAIL_HTLC(add.id, Right(TemporaryChannelFailure(d.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(TemporaryChannelFailure(d.channelUpdate)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -1301,11 +1302,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case PostRevocationAction.RelayHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: failing {}", add) - self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) case PostRevocationAction.RejectHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: rejecting {}", add) - self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 3d8116ad4a..da0395a3e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -23,9 +23,10 @@ import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractShared import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.send.Recipient import fr.acinq.eclair.router.Router.{BlindedHop, ChannelHop, Route} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.UseAttributableError 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, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomKey} import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} @@ -115,6 +116,10 @@ object IncomingPaymentPacket { case None => privateKey } decryptOnion(add.paymentHash, outerOnionDecryptionKey, add.onionRoutingPacket).flatMap { + case DecodedOnionPacket(payload, _) if payload.get[UseAttributableError].isDefined && !features.hasFeature(Features.AttributableError) => + Left(InvalidOnionPayload(UInt64(20), 0)) + case DecodedOnionPacket(payload, _) if payload.get[UseAttributableError].isEmpty && features.hasFeature(Features.AttributableError, Some(FeatureSupport.Mandatory)) => + Left(InvalidOnionPayload(UInt64(20), 0)) case DecodedOnionPacket(payload, Some(nextPacket)) => payload.get[OnionPaymentPayloadTlv.EncryptedRecipientData] match { case Some(_) if !features.hasFeature(Features.RouteBlinding) => Left(InvalidOnionPayload(UInt64(10), 0)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index ba87470c73..d84a265600 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -32,7 +32,6 @@ import fr.acinq.eclair.wire.protocol.{FailureMessage, InvalidOnionBlinding, Temp import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} import scala.concurrent.Promise -import scala.concurrent.duration.DurationInt import scala.util.Try /** @@ -132,7 +131,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) + // By default we use attributable errors if the feature is activated. + CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true) } channel ! cmd } else { @@ -262,7 +262,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = false, TimestampMilli.min) + // By default we use attributable errors if the feature is activated. + ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Origin.TrampolineRelayedCold(origins) => @@ -271,7 +272,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) + // By default we use attributable errors if the feature is activated. + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true)) } } } 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 f2c28a4201..fc30c729e0 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 @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, TimestampMilli} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, TimestampMilli} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -84,7 +84,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = false, TimestampMilli.now(), delay, commit = true) + // By default we use attributable errors if the feature is activated. + CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -92,7 +93,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // By default we use attributable errors if the feature is activated. + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index da56ee0765..29453df202 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -283,6 +283,7 @@ object PaymentOnion { records.records.find { case _: EncryptedRecipientData => false case _: BlindingPoint => false + case _: UseAttributableError => false case _ => true } match { case Some(_) => return Left(ForbiddenTlv(UInt64(0))) @@ -435,6 +436,7 @@ object PaymentOnion { case _: EncryptedRecipientData => false case _: BlindingPoint => false case _: TotalAmount => false + case _: UseAttributableError => false case _ => true } match { case Some(_) => return Left(ForbiddenTlv(UInt64(0)))