Skip to content

Commit e0a9c0a

Browse files
authored
Relay non-blinded failure from wallet nodes (#3050)
When using Bolt12 with a wallet node that is directly connected to us, we don't always need to return an unreadable failure upstream. If the wallet node sent us a normal failure, we relay it upstream instead of overriding it: they chose to reveal that failure to the payer so we should respect that. We make sure we don't leak a `channel_update` (since the channel was unannounced), and the wallet should encrypt the failure with their blinded node_id, so this shouldn't reveal information that wasn't already available in the invoice.
1 parent 8df52bb commit e0a9c0a

File tree

4 files changed

+32
-18
lines changed

4 files changed

+32
-18
lines changed

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -256,16 +256,30 @@ class ChannelRelay private(nodeParams: NodeParams,
256256
case _: CMD_FULFILL_HTLC => cmd
257257
case _: CMD_FAIL_HTLC | _: CMD_FAIL_MALFORMED_HTLC => r.payload match {
258258
case payload: IntermediatePayload.ChannelRelay.Blinded =>
259-
// We are inside a blinded route, so we must carefully choose the error we return to avoid leaking information.
260-
val failure = InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket))
261-
payload.records.get[OnionPaymentPayloadTlv.PathKey] match {
259+
walletNodeId_opt match {
262260
case Some(_) =>
263-
// We are the introduction node: we add a delay to make it look like it could come from further downstream.
264-
val delay = Some(Random.nextLong(1000).millis)
265-
CMD_FAIL_HTLC(cmd.id, FailureReason.LocalFailure(failure), delay, commit = true)
261+
// When the next node is a wallet node directly connected to us, we forward their failure downstream
262+
// because we don't need to protect the blinded path against probing since it only contains our node.
263+
// Their node isn't announced so they don't reveal anything by sending back a failure message (which
264+
// will be encrypted using their blinded node_id).
265+
cmd match {
266+
// However, when the failure comes from us, we don't want to leak the unannounced channel by revealing
267+
// its channel_update: in that case, we always return a temporary node failure instead.
268+
case cmd@CMD_FAIL_HTLC(_, FailureReason.LocalFailure(_: Update), _, _, _) => cmd.copy(reason = FailureReason.LocalFailure(TemporaryNodeFailure()))
269+
case _ => cmd
270+
}
266271
case None =>
267-
// We are not the introduction node.
268-
CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true)
272+
// We are inside a blinded route, so we must carefully choose the error we return to avoid leaking information.
273+
val failure = InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket))
274+
payload.records.get[OnionPaymentPayloadTlv.PathKey] match {
275+
case Some(_) =>
276+
// We are the introduction node: we add a delay to make it look like it could come from further downstream.
277+
val delay = Some(Random.nextLong(1000).millis)
278+
CMD_FAIL_HTLC(cmd.id, FailureReason.LocalFailure(failure), delay, commit = true)
279+
case None =>
280+
// We are not the introduction node.
281+
CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true)
282+
}
269283
}
270284
case _: IntermediatePayload.ChannelRelay.Standard => cmd
271285
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ object OnTheFlyFunding {
9696
def createFailureCommands(failure_opt: Option[FailureReason])(implicit log: LoggingAdapter): Seq[(ByteVector32, CMD_FAIL_HTLC)] = upstream match {
9797
case _: Upstream.Local => Nil
9898
case u: Upstream.Hot.Channel =>
99-
val failure = htlc.pathKey_opt match {
100-
case Some(_) => FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket)))
101-
case None => failure_opt.getOrElse(FailureReason.LocalFailure(UnknownNextPeer()))
102-
}
99+
// Note that even in the Bolt12 case, we relay the downstream failure instead of sending back invalid_onion_blinding.
100+
// That's because we are directly connected to the wallet: the blinded path doesn't contain any other public nodes,
101+
// so we don't need to protect against probing. This allows us to return a more meaningful failure to the payer.
102+
val failure = failure_opt.getOrElse(FailureReason.LocalFailure(UnknownNextPeer()))
103103
Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, commit = true))
104104
case u: Upstream.Hot.Trampoline =>
105105
val failure = failure_opt match {

eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
240240
cleanUpWakeUpActors(peerReadyManager, switchboard)
241241

242242
// We fail without attempting on-the-fly funding.
243-
expectFwdFail(register, r.add.channelId, CMD_FAIL_MALFORMED_HTLC(r.add.id, Sphinx.hash(r.add.onionRoutingPacket), InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)).code, commit = true))
243+
expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true))
244244
}
245245

246246
test("relay blinded payment (on-the-fly funding failed)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f =>
@@ -265,7 +265,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
265265
val fwdNodeId = register.expectMessageType[ForwardNodeId[Peer.ProposeOnTheFlyFunding]]
266266
assert(fwdNodeId.nodeId == outgoingNodeId)
267267
fwdNodeId.replyTo ! Register.ForwardNodeIdFailure(fwdNodeId)
268-
expectFwdFail(register, r.add.channelId, CMD_FAIL_MALFORMED_HTLC(r.add.id, Sphinx.hash(r.add.onionRoutingPacket), InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)).code, commit = true))
268+
expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true))
269269
}
270270

271271
test("relay blinded payment (on-the-fly funding not attempted)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f =>
@@ -292,7 +292,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
292292
fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, TooManyAcceptedHtlcs(channelIds(realScid1), 10), Some(u.channelUpdate))
293293

294294
// We fail without attempting on-the-fly funding.
295-
expectFwdFail(register, r.add.channelId, CMD_FAIL_MALFORMED_HTLC(r.add.id, Sphinx.hash(r.add.onionRoutingPacket), InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)).code, commit = true))
295+
expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true))
296296
}
297297

298298
test("relay with retries") { f =>
@@ -438,7 +438,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
438438
peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0)
439439
assert(switchboard.expectMessageType[Switchboard.GetPeerInfo].remoteNodeId == outgoingNodeId)
440440
val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
441-
assert(fail.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket))))
441+
assert(fail.message.reason == FailureReason.LocalFailure(UnknownNextPeer()))
442442

443443
cleanUpWakeUpActors(peerReadyManager, switchboard)
444444
}

eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
246246
val fwd2 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
247247
assert(fwd2.channelId == upstream2.add.channelId)
248248
assert(fwd2.message.id == upstream2.add.id)
249-
assert(fwd2.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(upstream2.add.onionRoutingPacket))))
249+
assert(fwd2.message.reason == FailureReason.EncryptedDownstreamFailure(fail2.reason))
250250

251251
val fail3 = WillFailMalformedHtlc(willAdd3.id, paymentHash, randomBytes32(), InvalidOnionHmac(randomBytes32()).code)
252252
peerConnection.send(peer, fail3)
@@ -330,7 +330,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
330330
val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
331331
assert(fwd.channelId == u.add.channelId)
332332
assert(fwd.message.id == u.add.id)
333-
assert(fwd.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket))))
333+
assert(fwd.message.reason == FailureReason.LocalFailure(UnknownNextPeer()))
334334
assert(fwd.message.commit)
335335
})
336336
peerConnection.expectMsgType[Warning]

0 commit comments

Comments
 (0)