Skip to content

Commit 8f586d5

Browse files
committed
Retransmit signatures during 0-conf disconnect
We must retransmit `tx_signatures` (and, if requested, `commit_sig`) after sending `channel_ready` in the 0-conf case.
1 parent 9080f98 commit 8f586d5

File tree

3 files changed

+122
-7
lines changed

3 files changed

+122
-7
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,14 +2324,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23242324
}
23252325

23262326
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) =>
2327-
log.debug("re-sending channelReady")
2327+
log.debug("re-sending channel_ready")
23282328
val channelReady = createChannelReady(d.aliases, d.commitments.params)
23292329
goto(WAIT_FOR_CHANNEL_READY) sending channelReady
23302330

2331-
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) =>
2332-
log.debug("re-sending channelReady")
2331+
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) =>
2332+
log.debug("re-sending channel_ready")
23332333
val channelReady = createChannelReady(d.aliases, d.commitments.params)
2334-
goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2334+
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
2335+
// and our commit_sig if they haven't received it already.
2336+
channelReestablish.nextFundingTxId_opt match {
2337+
case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId =>
2338+
d.commitments.latest.localFundingStatus.localSigs_opt match {
2339+
case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 =>
2340+
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2341+
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2342+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady)
2343+
case Some(txSigs) =>
2344+
log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2345+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady)
2346+
case None =>
2347+
log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus)
2348+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2349+
}
2350+
case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2351+
}
23352352

23362353
case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
23372354
Syncing.checkSync(keyManager, d.commitments, channelReestablish) match {

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import akka.testkit.{TestFSMRef, TestProbe}
2121
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, SatoshiLong, TxId}
2222
import fr.acinq.eclair.TestUtils.randomTxId
2323
import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet
24-
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished}
24+
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished, WatchPublishedTriggered}
2525
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2626
import fr.acinq.eclair.channel._
2727
import fr.acinq.eclair.channel.fsm.Channel
@@ -415,6 +415,59 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
415415
reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = false)
416416
}
417417

418+
test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
419+
import f._
420+
421+
alice2bob.expectMsgType[CommitSig]
422+
alice2bob.forward(bob)
423+
bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig
424+
bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures
425+
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED)
426+
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
427+
428+
// Note that this case can only happen when Bob doesn't need Alice's signatures to publish the transaction (when
429+
// Bob was the only one to contribute to the funding transaction).
430+
val fundingTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.tx.buildUnsignedTx()
431+
assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid)
432+
bob ! WatchPublishedTriggered(fundingTx)
433+
assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid)
434+
bob2alice.expectMsgType[ChannelReady]
435+
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY)
436+
437+
alice ! INPUT_DISCONNECTED
438+
awaitCond(alice.stateName == OFFLINE)
439+
bob ! INPUT_DISCONNECTED
440+
awaitCond(bob.stateName == OFFLINE)
441+
442+
val listener = TestProbe()
443+
alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
444+
445+
val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures())
446+
val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures())
447+
alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit)
448+
bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit)
449+
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
450+
assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTx.txid))
451+
assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
452+
alice2bob.forward(bob, channelReestablishAlice)
453+
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
454+
assert(channelReestablishBob.nextFundingTxId_opt.isEmpty)
455+
assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
456+
bob2alice.forward(alice, channelReestablishBob)
457+
458+
bob2alice.expectMsgType[CommitSig]
459+
bob2alice.forward(alice)
460+
bob2alice.expectMsgType[TxSignatures]
461+
bob2alice.forward(alice)
462+
alice2bob.expectMsgType[TxSignatures]
463+
alice2bob.forward(bob)
464+
465+
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
466+
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY)
467+
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid)
468+
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid)
469+
}
470+
418471
test("recv INPUT_DISCONNECTED (commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
419472
import f._
420473

@@ -448,7 +501,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
448501
bob2alice.forward(alice)
449502
bob2alice.expectMsgType[TxSignatures]
450503
bob2alice.forward(alice)
451-
alice2bob.expectMsgType[TxSignatures]
504+
alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures
452505
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
453506
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
454507

@@ -472,6 +525,51 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
472525
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
473526
}
474527

528+
test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
529+
import f._
530+
531+
val listener = TestProbe()
532+
bob.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
533+
534+
alice2bob.expectMsgType[CommitSig]
535+
alice2bob.forward(bob)
536+
bob2alice.expectMsgType[CommitSig]
537+
bob2alice.forward(alice)
538+
bob2alice.expectMsgType[TxSignatures]
539+
bob2alice.forward(alice)
540+
alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures
541+
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
542+
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
543+
544+
val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.signedTx_opt.get
545+
assert(alice2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid)
546+
alice ! WatchPublishedTriggered(fundingTx)
547+
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid)
548+
alice2bob.expectMsgType[ChannelReady]
549+
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY)
550+
551+
alice ! INPUT_DISCONNECTED
552+
awaitCond(alice.stateName == OFFLINE)
553+
bob ! INPUT_DISCONNECTED
554+
awaitCond(bob.stateName == OFFLINE)
555+
556+
val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures())
557+
val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures())
558+
alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit)
559+
bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit)
560+
561+
assert(alice2bob.expectMsgType[ChannelReestablish].nextFundingTxId_opt.isEmpty)
562+
alice2bob.forward(bob)
563+
assert(bob2alice.expectMsgType[ChannelReestablish].nextFundingTxId_opt.contains(fundingTx.txid))
564+
bob2alice.forward(alice)
565+
alice2bob.expectMsgType[TxSignatures]
566+
alice2bob.forward(bob)
567+
alice2bob.expectMsgType[ChannelReady]
568+
alice2bob.forward(bob)
569+
assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid)
570+
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid)
571+
}
572+
475573
private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = {
476574
import f._
477575

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2200,7 +2200,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
22002200
resolveHtlcs(f, htlcs)
22012201
}
22022202

2203-
test("disconnect (RBF commit_sig received by bob)", Tag(ChannelStateTestsTags.FundingDeeplyBuried)) { f =>
2203+
test("disconnect (RBF commit_sig received by bob)") { f =>
22042204
import f._
22052205

22062206
val htlcs = setupHtlcs(f)

0 commit comments

Comments
 (0)