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:
parent
648f93f682
commit
8a65e35c8f
3 changed files with 35 additions and 11 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue