From d894757647bae1f5e574264d5adc5a59ab030d67 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 20 Feb 2026 16:57:16 +0100 Subject: [PATCH] Fix flaky wallet funding tests We fix a few flaky tests in our channel FSM that test scenarios where errors are received while creating funding transactions. The issue was that we used a dummy wallet that could complete its call before our calls to `awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)`. We use a new dummy on-chain wallet that never responds to transaction funding calls to allow tests to inject failure events. --- .../acinq/eclair/blockchain/DummyOnChainWallet.scala | 10 +++++++++- .../states/a/WaitForAcceptChannelStateSpec.scala | 11 +++++++---- .../states/b/WaitForFundingInternalStateSpec.scala | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index c29ffa4a3f..1ee99dbb26 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.{TimestampSecond, randomBytes32} import scodec.bits._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.{Failure, Random, Success} /** @@ -229,6 +229,14 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnChainAddressCache { override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = p2trScript } +/** A wallet that blocks when called to fund transactions (useful to test events happening while funding). */ +class BlockingOnChainWallet extends SingleKeyOnChainWallet { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + // We create a dummy promise that will never be completed. + Promise().future + } +} + object DummyOnChainWallet { val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 07b371dcca..94e0953810 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -20,6 +20,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, TxId} import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.blockchain.BlockingOnChainWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -40,6 +41,7 @@ import scala.concurrent.duration._ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + private val BlockingOnChainWallet = "blocking_on_chain_wallet" private val HighRemoteDustLimit = "high_remote_dust_limit" case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) @@ -48,7 +50,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams.modify(_.channelConf.maxRemoteDustLimit).setToIf(test.tags.contains(HighRemoteDustLimit))(15_000 sat) - val setup = init(aliceNodeParams, Bob.nodeParams, tags = test.tags) + val wallet_opt = if (test.tags.contains(BlockingOnChainWallet)) Some(new BlockingOnChainWallet()) else None + val setup = init(aliceNodeParams, Bob.nodeParams, tags = test.tags, walletA_opt = wallet_opt) import setup._ val channelParams = computeChannelParams(setup, test.tags) @@ -64,7 +67,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS } } - test("recv AcceptChannel (anchor outputs zero fee htlc txs)") { f => + test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(BlockingOnChainWallet)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. @@ -76,7 +79,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.ScidAlias)) { f => + test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.ScidAlias), Tag(BlockingOnChainWallet)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) @@ -86,7 +89,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (simple taproot channels phoenix)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + test("recv AcceptChannel (simple taproot channels phoenix)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix), Tag(BlockingOnChainWallet)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsPhoenix)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index dc831d481d..5c1cb3f245 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.blockchain.BlockingOnChainWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -40,7 +41,8 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val setup = init(tags = test.tags) + // Note that we use a dummy wallet that doesn't complete its transaction funding. + val setup = init(tags = test.tags, walletA_opt = Some(new BlockingOnChainWallet())) import setup._ val channelParams = computeChannelParams(setup, test.tags) val listener = TestProbe()