diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 88f3a2ad84..5f576c6383 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.ClosingTx -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, Warning} import java.sql.SQLException @@ -134,7 +134,16 @@ trait ErrorHandlers extends CommonHandlers { // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) - case hasCommitments: ChannelDataWithCommitments => spendLocalCurrent(hasCommitments) + case hasCommitments: ChannelDataWithCommitments => + if (e.toAscii == "internal error") { + // It seems like lnd sends this error whenever something wrong happens on their side, regardless of whether + // the channel actually needs to be closed. We ignore it to avoid paying the cost of a channel force-close, + // it's up to them to broadcast their commitment if they wish. + log.warning("ignoring remote 'internal error', probably coming from lnd") + stay() sending Warning(d.channelId, "ignoring your 'internal error' to avoid an unnecessary force-close") + } else { + spendLocalCurrent(hasCommitments) + } // When there is no commitment yet, we just go to CLOSED state in case an error occurs. case _: ChannelDataWithoutCommitments => goto(CLOSED) case _: TransientChannelData => goto(CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 1f47e3e4ad..8d8eb4d8f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -3342,6 +3342,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(1 second) } + test("recv Error (ignored internal error from lnd)") { f => + import f._ + + alice ! Error(channelId(alice), "internal error") + alice2bob.expectMsgType[Warning] + alice2blockchain.expectNoMessage(100 millis) + } + def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = { import f._