1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 06:35:11 +01:00

Don't broadcast commit before funding locked (#2163)

If a channel is force-closed before the funding transaction is confirmed,
broadcasting our local commit can be a problem if the funding
tx is double spent. When that happens, the channel stays stuck in the
closing state trying to publish a commit tx with an invalid input.

If we haven't even seen the funding tx in the mempool, we have no way of
being sure that it was double spent, so we would need to keep trying
forever, which pollutes the logs with publishing errors.

Whenever the funding transaction isn't confirmed and we have nothing
at stake, we now directly go to the closed state without publishing our
commitment. This will be an issue for peers who lost state and rely on
us for dataloss protection, but it's not worth exposing ourselves to that
annoying edge case. Our peers should be able to at least keep state
long enough for the funding tx to confirm or for them to force-close.
This commit is contained in:
Bastien Teinturier 2022-02-02 17:28:03 +01:00 committed by GitHub
parent 648f93f682
commit 8a65e35c8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 35 additions and 11 deletions

View file

@ -2391,6 +2391,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that // 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)) handleMutualClose(bestUnpublishedClosingTx, Left(negotiating))
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent
case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // 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: HasCommitments => spendLocalCurrent(hasCommitments) // 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 _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs
} }

View file

@ -23,11 +23,11 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT}
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.transactions.Scripts.multiSig2of2
import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel}
import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey}
import org.scalatest.Outcome import org.scalatest.{Outcome, Tag}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -38,7 +38,7 @@ import scala.concurrent.duration._
class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase {
case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, listener: TestProbe)
override def withFixture(test: OneArgTest): Outcome = { override def withFixture(test: OneArgTest): Outcome = {
@ -46,13 +46,15 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
import setup._ import setup._
val channelConfig = ChannelConfig.standard val channelConfig = ChannelConfig.standard
val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures) val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures) val bobInit = Init(bobParams.initFeatures)
within(30 seconds) { within(30 seconds) {
val listener = TestProbe() val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
@ -65,11 +67,14 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
bob2alice.expectMsgType[FundingSigned] bob2alice.expectMsgType[FundingSigned]
bob2alice.forward(alice) bob2alice.forward(alice)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
alice2blockchain.expectMsgType[WatchFundingSpent] alice2blockchain.expectMsgType[WatchFundingSpent]
bob2blockchain.expectMsgType[WatchFundingSpent]
alice2blockchain.expectMsgType[WatchFundingConfirmed] alice2blockchain.expectMsgType[WatchFundingConfirmed]
bob2blockchain.expectMsgType[WatchFundingConfirmed]
listener.expectMsgType[TransactionPublished] // alice has published the funding transaction listener.expectMsgType[TransactionPublished] // alice has published the funding transaction
awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, listener))) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, listener)))
} }
} }
@ -208,6 +213,13 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
} }
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f =>
import f._
bob ! Error(ByteVector32.Zeroes, "funding double-spent")
bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake
awaitCond(bob.stateName == CLOSED)
}
test("recv CMD_CLOSE") { f => test("recv CMD_CLOSE") { f =>
import f._ import f._
val sender = TestProbe() val sender = TestProbe()

View file

@ -21,12 +21,12 @@ import fr.acinq.bitcoin.{ByteVector32, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -38,18 +38,20 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
val relayFees: RelayFees = RelayFees(999 msat, 1234) val relayFees: RelayFees = RelayFees(999 msat, 1234)
case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe)
override def withFixture(test: OneArgTest): Outcome = { override def withFixture(test: OneArgTest): Outcome = {
val setup = init() val setup = init()
import setup._ import setup._
val channelConfig = ChannelConfig.standard val channelConfig = ChannelConfig.standard
val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat
val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures) val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures) val bobInit = Init(bobParams.initFeatures)
within(30 seconds) { within(30 seconds) {
alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
@ -76,7 +78,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
alice2bob.expectMsgType[FundingLocked] alice2bob.expectMsgType[FundingLocked]
awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED)
awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, router))) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router)))
} }
} }
@ -121,6 +123,15 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
} }
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! Error(ByteVector32.Zeroes, "funding double-spent")
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}
test("recv CMD_CLOSE") { f => test("recv CMD_CLOSE") { f =>
import f._ import f._
val sender = TestProbe() val sender = TestProbe()