From 9625a6e8c6bdb56dfd8d4d1102007dcb9cf054e7 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 13 Dec 2024 10:16:16 +0100 Subject: [PATCH 1/2] Remove spurious interactive-tx `commit_sig` retransmission We fully implement https://github.com/lightning/bolts/pull/1214 to stop retransmitting `commit_sig` when our peer has already received it. We also correctly set `next_commitment_number` to let our peer know whether we have received their `commit_sig` or not. We also retransmit `tx_signatures` (and, if requested, `commit_sig`) after sending `channel_ready` in the 0-conf case. This was missing and was a bug. --- .../fr/acinq/eclair/channel/fsm/Channel.scala | 105 +++++++++---- .../channel/fund/InteractiveTxBuilder.scala | 5 + .../b/WaitForDualFundingSignedStateSpec.scala | 129 +++++++++++++--- ...WaitForDualFundingConfirmedStateSpec.scala | 53 +++++-- .../states/e/NormalSplicesStateSpec.scala | 146 +++++++++--------- 5 files changed, 298 insertions(+), 140 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index ac2ad76d44..4164706365 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -2242,7 +2242,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId)) val channelReestablish = ChannelReestablish( channelId = d.channelId, - nextLocalCommitmentNumber = 1, + nextLocalCommitmentNumber = d.signingSession.reconnectNextLocalCommitmentNumber, nextRemoteRevocationNumber = 0, yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, @@ -2257,6 +2257,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val yourLastPerCommitmentSecret = remotePerCommitmentSecrets.lastIndex.flatMap(remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig) val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommitIndex) + // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig. + val nextLocalCommitmentNumber = d match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match { + case DualFundingStatus.RbfWaitingForSigs(status) => status.reconnectNextLocalCommitmentNumber + case _ => d.commitments.localCommitIndex + 1 + } + case d: DATA_NORMAL => d.spliceStatus match { + case SpliceStatus.SpliceWaitingForSigs(status) => status.reconnectNextLocalCommitmentNumber + case _ => d.commitments.localCommitIndex + 1 + } + case _ => d.commitments.localCommitIndex + 1 + } + // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures. val rbfTlv: Set[ChannelReestablishTlv] = d match { case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match { case DualFundingStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) @@ -2280,7 +2293,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val channelReestablish = ChannelReestablish( channelId = d.channelId, - nextLocalCommitmentNumber = d.commitments.localCommitIndex + 1, + nextLocalCommitmentNumber = nextLocalCommitmentNumber, nextRemoteRevocationNumber = d.commitments.remoteCommitIndex, yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, @@ -2321,8 +2334,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId => - // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. + case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => + // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received + // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) @@ -2333,20 +2347,25 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Some(fundingTxId) => d.status match { case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig + if (channelReestablish.nextLocalCommitmentNumber == 0) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig + } else { + // They have already received our commit_sig, but we were waiting for them to send either commit_sig or + // tx_signatures first. We wait for their message before sending our tx_signatures. + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => - val toSend = d.latestFundingTx.sharedTx match { - case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction => - // We have not received their tx_signatures: we retransmit our commit_sig because we don't know if they received it. - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) - Seq(commitSig, fundingTx.localSigs) - case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction => - // We've already received their tx_signatures, which means they've received and stored our commit_sig, we only need to retransmit our tx_signatures. - Seq(fundingTx.localSigs) + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == 0) { + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) + } else { + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs } - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending toSend case _ => // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving // their tx_complete): we tell them to abort that RBF attempt. @@ -2356,14 +2375,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => - log.debug("re-sending channelReady") + log.debug("re-sending channel_ready") val channelReady = createChannelReady(d.aliases, d.commitments.params) goto(WAIT_FOR_CHANNEL_READY) sending channelReady - case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => - log.debug("re-sending channelReady") + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => + log.debug("re-sending channel_ready") val channelReady = createChannelReady(d.aliases, d.commitments.params) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId => + d.commitments.latest.localFundingStatus.localSigs_opt match { + case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) + case Some(txSigs) => + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady) + case None => + log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + } + case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + } case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { @@ -2389,23 +2425,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Some(fundingTxId) => d.spliceStatus match { case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) - sendQueue = sendQueue :+ commitSig + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) + val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) + sendQueue = sendQueue :+ commitSig + } d.spliceStatus case _ if d.commitments.latest.fundingTxId == fundingTxId => d.commitments.latest.localFundingStatus match { case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx => - dfu.sharedTx match { - case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction => - // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it - log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) - sendQueue = sendQueue :+ commitSig :+ fundingTx.localSigs - case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction => - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - sendQueue = sendQueue :+ fundingTx.localSigs + // We've already received their commit_sig and sent our tx_signatures. We retransmit our + // tx_signatures and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs + } else { + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + sendQueue = sendQueue :+ dfu.sharedTx.localSigs } case fundingStatus => // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 29f41c4a7b..f2ed77db59 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -1082,6 +1082,11 @@ object InteractiveTxSigningSession { liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input) val localCommitIndex: Long = localCommit.fold(_.index, _.index) + // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. + val reconnectNextLocalCommitmentNumber: Long = localCommit match { + case Left(commit) => commit.index + case Right(commit) => commit.index + 1 + } def receiveCommitSig(nodeParams: NodeParams, channelParams: ChannelParams, remoteCommitSig: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = { localCommit match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala index ceb7082d21..3f4964fd4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -21,7 +21,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, SatoshiLong, TxId} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished, WatchPublishedTriggered} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel @@ -375,15 +375,16 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId) + reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = true) } - test("recv INPUT_DISCONNECTED (commit_sig not received, next_commitment_number = 0)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig - bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) @@ -392,10 +393,10 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceCommitmentNumber = 0, bobCommitmentNumber = 0) + reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = true) } - test("recv INPUT_DISCONNECTED (commit_sig partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -411,13 +412,12 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId) + reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = false) } - test("recv INPUT_DISCONNECTED (commit_sig partially received, next_commitment_number = 0)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig @@ -425,12 +425,47 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + // Note that this case can only happen when Bob doesn't need Alice's signatures to publish the transaction (when + // Bob was the only one to contribute to the funding transaction). + val fundingTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.tx.buildUnsignedTx() + assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) + bob ! WatchPublishedTriggered(fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) + bob2alice.expectMsgType[ChannelReady] + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) + alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == OFFLINE) bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceCommitmentNumber = 0) + val listener = TestProbe() + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTx.txid)) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) + alice2bob.forward(bob, channelReestablishAlice) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) + assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + bob2alice.forward(alice, channelReestablishBob) + + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) + assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid) } test("recv INPUT_DISCONNECTED (commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -450,7 +485,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId) + reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = false) } test("recv INPUT_DISCONNECTED (tx_signatures received)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -466,7 +501,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) - alice2bob.expectMsgType[TxSignatures] + alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) @@ -490,7 +525,52 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId) } - private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceCommitmentNumber: Long = 1, bobCommitmentNumber: Long = 1): Unit = { + test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val listener = TestProbe() + bob.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.signedTx_opt.get + assert(alice2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) + alice ! WatchPublishedTriggered(fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) + alice2bob.expectMsgType[ChannelReady] + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + + assert(alice2bob.expectMsgType[ChannelReestablish].nextFundingTxId_opt.isEmpty) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[ChannelReestablish].nextFundingTxId_opt.contains(fundingTx.txid)) + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + alice2bob.expectMsgType[ChannelReady] + alice2bob.forward(bob) + assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) + assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid) + } + + private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = { import f._ val listener = TestProbe() @@ -501,17 +581,24 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + val nextLocalCommitmentNumberAlice = if (aliceExpectsCommitSig) 0 else 1 assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTxId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) - alice2bob.forward(bob, channelReestablishAlice.copy(nextLocalCommitmentNumber = aliceCommitmentNumber)) + assert(channelReestablishAlice.nextLocalCommitmentNumber == nextLocalCommitmentNumberAlice) + alice2bob.forward(bob, channelReestablishAlice) val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + val nextLocalCommitmentNumberBob = if (bobExpectsCommitSig) 0 else 1 assert(channelReestablishBob.nextFundingTxId_opt.contains(fundingTxId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == 1) - bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitmentNumber)) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + assert(channelReestablishBob.nextLocalCommitmentNumber == nextLocalCommitmentNumberBob) + bob2alice.forward(alice, channelReestablishBob) + + if (aliceExpectsCommitSig) { + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + } + if (bobExpectsCommitSig) { + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + } bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index d3af777ed7..fb451cbb21 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -933,7 +933,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) @@ -948,7 +948,43 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectNoMessage(100 millis) } - test("recv INPUT_DISCONNECTED (rbf commit_sig partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + initiateRbf(f) + alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + + val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) + assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) + assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishBob.nextLocalCommitmentNumber == 0) + + // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. + bob2alice.expectNoMessage(100 millis) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + val nextFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] + assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid) + assert(bobListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid) + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ initiateRbf(f) @@ -964,15 +1000,14 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) - // Alice and Bob exchange signatures and complete the RBF attempt. - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + // Bob retransmits commit_sig and tx_signatures, then Alice sends her tx_signatures. bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] @@ -1007,11 +1042,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTx.txId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) - // Alice and Bob exchange signatures and complete the RBF attempt. - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + // Alice and Bob exchange tx_signatures and complete the RBF attempt. bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 8bd96d1c2b..4edcb9e0c3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -1767,57 +1767,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) - assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) - - // Alice and Bob retransmit commit_sig and tx_signatures. - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - bob2alice.expectMsgType[TxSignatures] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxSignatures] - alice2bob.forward(bob) - sender.expectMsgType[RES_SPLICE] - - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get - alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) - bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) - alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) - alice2bob.expectMsgType[SpliceLocked] - alice2bob.forward(bob) - bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) - bob2alice.expectMsgType[SpliceLocked] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - - resolveHtlcs(f, htlcs) - } - - test("disconnect (commit_sig not received, reestablish with previous commitment_number)") { f => - import f._ - - val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - - val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) - alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig - bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) - val spliceStatus = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs] - - disconnect(f) - val (channelReestablishAlice, channelReestablishBob) = reconnect(f, sendReestablish = false) - assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) - alice2bob.forward(bob, channelReestablishAlice.copy(nextLocalCommitmentNumber = aliceCommitIndex)) + assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) - bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitIndex)) + assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) // Alice and Bob retransmit commit_sig and tx_signatures. alice2bob.expectMsgType[CommitSig] @@ -1865,13 +1817,13 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) - // Alice and Bob retransmit commit_sig and tx_signatures. + // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. + bob2alice.expectNoMessage(100 millis) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] @@ -1893,7 +1845,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received by alice, reestablish with previous commitment_number)") { f => + test("disconnect (commit_sig received by bob)") { f => import f._ val htlcs = setupHtlcs(f) @@ -1902,26 +1854,25 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(aliceCommitIndex != bobCommitIndex) val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) - alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) val spliceStatus = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs] disconnect(f) - val (channelReestablishAlice, channelReestablishBob) = reconnect(f, sendReestablish = false) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) - alice2bob.forward(bob, channelReestablishAlice) + assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) - bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitIndex)) - // Alice and Bob retransmit commit_sig and tx_signatures. - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + // Bob retransmit commit_sig and tx_signatures, Alice sends tx_signatures afterwards. bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] @@ -1943,7 +1894,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (tx_signatures sent by bob)") { f => + test("disconnect (commit_sig received)") { f => import f._ val htlcs = setupHtlcs(f) @@ -1966,11 +1917,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) bob2blockchain.expectWatchFundingConfirmed(spliceTxId) - // Alice and Bob retransmit commit_sig and tx_signatures. - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + // Alice and Bob retransmit tx_signatures. + alice2bob.expectNoMessage(100 millis) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] @@ -2184,9 +2132,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) - assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) + assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) // Alice and Bob retransmit commit_sig and tx_signatures. @@ -2233,14 +2181,62 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) - assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) - // Alice and Bob retransmit commit_sig and tx_signatures. + // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. + bob2alice.expectNoMessage(100 millis) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + probe.expectMsgType[RES_SPLICE] + + val rbfTx = confirmRbfTx(f) + assert(rbfTx.txid != spliceTx.txid) + resolveHtlcs(f, htlcs) + } + + test("disconnect (RBF commit_sig received by bob)") { f => + import f._ + + val htlcs = setupHtlcs(f) + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == spliceTx.txid) + + // Bob uses the channel before Alice tries to RBF. + val (_, add) = addHtlc(40_000_000 msat, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + failHtlc(add.id, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + + val probe = initiateRbfWithoutSigs(f, FeeratePerKw(15_000 sat), sInputsCount = 2, sOutputsCount = 2) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val rbfTxId = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs].signingSession.fundingTx.txId + + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) + assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) + + // Bob retransmits commit_sig, and they exchange tx_signatures afterwards. bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] From c9819404bd5de4a783381be9c311eea4170b417d Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Jul 2024 16:00:01 +0200 Subject: [PATCH 2/2] Use official splice messages We replace our experimental version of `splice_init`, `splice_ack` and `splice_locked` by their official version. If our peer is using the experimental feature bit, we convert our outgoing messages to use the experimental encoding and incoming messages to the official messages. We also change the TLV fields added to `tx_add_input`, `tx_signatures` and `splice_locked` to match the spec version. We always write both the official and experimental TLV to updated nodes (because the experimental one is odd and will be ignored) but we drop the official TLV if our peer is using the experimental feature, because it won't understand the even TLV field. This guarantees backwards-compatibility with peers who only support the experimental feature. --- docs/release-notes/eclair-vnext.md | 33 ++++++++++++ eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Features.scala | 17 +++---- .../fr/acinq/eclair/channel/Commitments.scala | 14 +++-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 4 +- .../fr/acinq/eclair/io/PeerConnection.scala | 27 +++++++--- .../acinq/eclair/wire/protocol/HtlcTlv.scala | 22 +++++--- .../wire/protocol/InteractiveTxTlv.scala | 12 ++++- .../protocol/LightningMessageCodecs.scala | 28 ++++++++-- .../wire/protocol/LightningMessageTypes.scala | 51 +++++++++++++++++-- .../fundee/data.json | 6 +-- .../funder/data.json | 6 +-- .../fundee/data.json | 6 +-- .../funder/data.json | 6 +-- .../04000e-DATA_NORMAL/announced/data.json | 6 +-- .../splicing-private/data.json | 6 +-- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../basic/channel/GossipIntegrationSpec.scala | 2 - .../io/OpenChannelInterceptorSpec.scala | 4 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 47 ++++++++++++++++- .../payment/relay/OnTheFlyFundingSpec.scala | 6 +-- .../protocol/LightningMessageCodecsSpec.scala | 41 ++++++++------- 22 files changed, 258 insertions(+), 91 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 133362df1b..0dc96e255f 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -21,6 +21,39 @@ Once closing transactions are broadcast, they can be RBF-ed by calling the `clos ./eclair-cli close --channelId= --preferredFeerateSatByte= ``` +### Channel Splicing + +With this release, we add support for the final version of [splicing](https://github.com/lightning/bolts/pull/1160) that was recently added to the BOLTs. +Splicing allows node operators to change the size of their existing channels, which makes it easier and more efficient to allocate liquidity where it is most needed. +Most node operators can now have a single channel with each of their peer, which costs less on-chain fees and resources, and makes path-finding easier. + +The size of an existing channel can be increased with the `splicein` API: + +```sh +eclair-cli splicein --channelId= --amountIn= +``` + +Once that transaction confirms, the additional liquidity can be used to send outgoing payments. +If the transaction doesn't confirm, the node operator can speed up confirmation with the `rbfsplice` API: + +```sh +eclair-cli rbfsplice --channelId= --targetFeerateSatByte= --fundingFeeBudgetSatoshis= +``` + +If the node operator wants to reduce the size of a channel, or send some of the channel funds to an on-chain address, they can use the `spliceout` API: + +```sh +eclair-cli spliceout --channelId= --amountOut= --scriptPubKey= +``` + +That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary. + +Note that when 0-conf is used for the channel, it is not possible to RBF splice transactions. +Node operators should instead create a new splice transaction (with `splicein` or `spliceout`) to CPFP the previous transaction. + +Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal. +We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions. + ### Peer storage With this release, eclair supports the `option_provide_storage` feature introduced in . diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 403c8d5e3f..14a66bdd39 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -85,6 +85,7 @@ eclair { option_zeroconf = disabled keysend = disabled option_simple_close=optional + option_splice = optional trampoline_payment_prototype = disabled async_payment_prototype = disabled on_the_fly_funding = disabled diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index b7b60e287c..9fc5329e18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -264,8 +264,7 @@ object Features { val mandatory = 28 } - // TODO: this should also extend NodeFeature once the spec is finalized - case object Quiescence extends Feature with InitFeature { + case object Quiescence extends Feature with InitFeature with NodeFeature { val rfcName = "option_quiesce" val mandatory = 34 } @@ -310,6 +309,11 @@ object Features { val mandatory = 60 } + case object Splicing extends Feature with InitFeature with NodeFeature { + val rfcName = "option_splice" + val mandatory = 62 + } + /** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */ case object WakeUpNotificationClient extends Feature with InitFeature { val rfcName = "wake_up_notification_client" @@ -333,12 +337,6 @@ object Features { val mandatory = 152 } - // TODO: @pm47 custom splices implementation for phoenix, to be replaced once splices is spec-ed (currently reserved here: https://github.com/lightning/bolts/issues/605) - case object SplicePrototype extends Feature with InitFeature { - val rfcName = "splice_prototype" - val mandatory = 154 - } - /** * Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md. * TODO: add NodeFeature once bLIP is merged. @@ -381,10 +379,10 @@ object Features { ZeroConf, KeySend, SimpleClose, + Splicing, WakeUpNotificationClient, TrampolinePaymentPrototype, AsyncPaymentPrototype, - SplicePrototype, OnTheFlyFunding, FundingFeeCredit ) @@ -401,7 +399,6 @@ object Features { KeySend -> (VariableLengthOnion :: Nil), SimpleClose -> (ShutdownAnySegwit :: Nil), AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil), - OnTheFlyFunding -> (SplicePrototype :: Nil), FundingFeeCredit -> (OnTheFlyFunding :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 1ce208edcf..64baacf7da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -682,9 +682,11 @@ case class Commitment(fundingTxIndex: Long, log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(",")) Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) - val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set( - if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None - ).flatten[CommitSigTlv])) + val tlvs = Set( + if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize, fundingTxId)) else None, + if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None, + ).flatten[CommitSigTlv] + val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs)) val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig) } @@ -1081,8 +1083,10 @@ case class Commitments(params: ChannelParams, } val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1) - // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. - val active1 = active.zip(commits).map { case (commitment, commit) => + val active1 = active.zipWithIndex.map { case (commitment, idx) => + // If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first). + // This matches the behavior of peers who only support the experimental version of splicing. + val commit = commits.find(_.fundingTxId_opt.contains(commitment.fundingTxId)).getOrElse(commits(idx)) commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match { case Left(f) => return Left(f) case Right(commitment1) => commitment1 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 4164706365..26cf1375eb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -878,7 +878,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(cmd: CMD_SPLICE, d: DATA_NORMAL) => - if (!d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype)) { + if (!d.commitments.params.remoteParams.initFeatures.hasFeature(Features.Splicing)) { log.warning("cannot initiate splice, peer doesn't support splicing") cmd.replyTo ! RES_FAILURE(cmd, CommandUnavailableInThisState(d.channelId, "splice", NORMAL)) stay() @@ -2991,7 +2991,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with /** For splices we will send one commit_sig per active commitments. */ private def aggregateSigs(commit: CommitSig): Option[Seq[CommitSig]] = { sigStash = sigStash :+ commit - log.debug("received sig for batch of size={}", commit.batchSize) + log.debug("received sig for batch of size={} for fundingTxId={}", commit.batchSize, commit.fundingTxId_opt) if (sigStash.size == commit.batchSize) { val sigs = sigStash sigStash = Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index ffcf92cac7..14d8f9a29b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.io import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated} import akka.event.Logging.MDC -import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32} +import fr.acinq.bitcoin.scalacompat.BlockHash import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureCompatibilityResult, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector @@ -206,11 +206,20 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A stay() case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message - d.transport forward msg + val useExperimentalSplice = d.remoteInit.features.unknown.map(_.bitIndex).contains(155) + msg match { + // If our peer is using the experimental splice version, we convert splice messages. + case msg: SpliceInit if useExperimentalSplice => d.transport forward ExperimentalSpliceInit.from(msg) + case msg: SpliceAck if useExperimentalSplice => d.transport forward ExperimentalSpliceAck.from(msg) + case msg: SpliceLocked if useExperimentalSplice => d.transport forward ExperimentalSpliceLocked.from(msg) + case msg: TxAddInput if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxAddInputTlv.SharedInputTxId]))) + case msg: CommitSig if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.BatchTlv]))) + case msg: TxSignatures if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxSignaturesTlv.PreviousFundingTxSig]))) + case _ => d.transport forward msg + } msg match { // If we send any channel management message to this peer, the connection should be persistent. - case _: ChannelMessage if !d.isPersistent => - stay() using d.copy(isPersistent = true) + case _: ChannelMessage if !d.isPersistent => stay() using d.copy(isPersistent = true) case _ => stay() } @@ -343,7 +352,13 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A case Event(msg: LightningMessage, d: ConnectedData) => // we acknowledge and pass all other messages to the peer d.transport ! TransportHandler.ReadAck(msg) - d.peer ! msg + msg match { + // If our peer is using the experimental splice version, we convert splice messages. + case msg: ExperimentalSpliceInit => d.peer ! msg.toSpliceInit() + case msg: ExperimentalSpliceAck => d.peer ! msg.toSpliceAck() + case msg: ExperimentalSpliceLocked => d.peer ! msg.toSpliceLocked() + case _ => d.peer ! msg + } stay() case Event(readAck: TransportHandler.ReadAck, d: ConnectedData) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 7e608d36e5..c8f48c3bd5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.TxId import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16} @@ -73,16 +74,25 @@ object UpdateFailMalformedHtlcTlv { sealed trait CommitSigTlv extends Tlv object CommitSigTlv { + /** + * While a splice is ongoing and not locked, we have multiple valid commitments. + * We send one [[CommitSig]] message for each valid commitment. + * + * @param size the number of [[CommitSig]] messages in the batch. + * @param fundingTxId the funding transaction spent by this commitment. + */ + case class BatchTlv(size: Int, fundingTxId: TxId) extends CommitSigTlv - /** @param size the number of [[CommitSig]] messages in the batch */ - case class BatchTlv(size: Int) extends CommitSigTlv + private val batchTlv: Codec[BatchTlv] = tlvField(uint16 :: txIdAsHash) - object BatchTlv { - val codec: Codec[BatchTlv] = tlvField(tu16) - } + /** Similar to [[BatchTlv]] for peers who only support the experimental version of splicing. */ + case class ExperimentalBatchTlv(size: Int) extends CommitSigTlv + + private val experimentalBatchTlv: Codec[ExperimentalBatchTlv] = tlvField(tu16) val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint) - .typecase(UInt64(0x47010005), BatchTlv.codec) + .typecase(UInt64(0), batchTlv) + .typecase(UInt64(0x47010005), experimentalBatchTlv) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala index 96696d8356..97c77e5ae5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -33,9 +33,13 @@ object TxAddInputTlv { /** When doing a splice, the initiator must provide the previous funding txId instead of the whole transaction. */ case class SharedInputTxId(txId: TxId) extends TxAddInputTlv + /** Same as [[SharedInputTxId]] for peers who only support the experimental version of splicing. */ + case class ExperimentalSharedInputTxId(txId: TxId) extends TxAddInputTlv + val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint) // Note that we actually encode as a tx_hash to be consistent with other lightning messages. - .typecase(UInt64(1105), tlvField(txIdAsHash.as[SharedInputTxId])) + .typecase(UInt64(0), tlvField(txIdAsHash.as[SharedInputTxId])) + .typecase(UInt64(1105), tlvField(txIdAsHash.as[ExperimentalSharedInputTxId])) ) } @@ -69,8 +73,12 @@ object TxSignaturesTlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv + /** Same as [[PreviousFundingTxSig]] for peers who only support the experimental version of splicing. */ + case class ExperimentalPreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint) - .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig])) + .typecase(UInt64(0), tlvField(bytes64.as[PreviousFundingTxSig])) + .typecase(UInt64(601), tlvField(bytes64.as[ExperimentalPreviousFundingTxSig])) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 0928d8ae35..45c9a3ac71 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -434,17 +434,36 @@ object LightningMessageCodecs { ("fundingPubkey" | publicKey) :: ("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[SpliceInit] + val experimentalSpliceInitCodec: Codec[ExperimentalSpliceInit] = ( + ("channelId" | bytes32) :: + ("fundingContribution" | satoshiSigned) :: + ("feerate" | feeratePerKw) :: + ("lockTime" | uint32) :: + ("fundingPubkey" | publicKey) :: + ("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[ExperimentalSpliceInit] + val spliceAckCodec: Codec[SpliceAck] = ( ("channelId" | bytes32) :: ("fundingContribution" | satoshiSigned) :: ("fundingPubkey" | publicKey) :: ("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[SpliceAck] + val experimentalSpliceAckCodec: Codec[ExperimentalSpliceAck] = ( + ("channelId" | bytes32) :: + ("fundingContribution" | satoshiSigned) :: + ("fundingPubkey" | publicKey) :: + ("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[ExperimentalSpliceAck] + val spliceLockedCodec: Codec[SpliceLocked] = ( ("channelId" | bytes32) :: ("fundingTxHash" | txIdAsHash) :: ("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[SpliceLocked] + val experimentalSpliceLockedCodec: Codec[ExperimentalSpliceLocked] = ( + ("channelId" | bytes32) :: + ("fundingTxHash" | txIdAsHash) :: + ("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[ExperimentalSpliceLocked] + val stfuCodec: Codec[Stfu] = ( ("channelId" | bytes32) :: ("initiator" | byte.xmap[Boolean](b => b != 0, b => if (b) 1 else 0))).as[Stfu] @@ -526,6 +545,9 @@ object LightningMessageCodecs { .typecase(72, txInitRbfCodec) .typecase(73, txAckRbfCodec) .typecase(74, txAbortCodec) + .typecase(77, spliceLockedCodec) + .typecase(80, spliceInitCodec) + .typecase(81, spliceAckCodec) .typecase(128, updateAddHtlcCodec) .typecase(130, updateFulfillHtlcCodec) .typecase(131, updateFailHtlcCodec) @@ -557,9 +579,9 @@ object LightningMessageCodecs { .typecase(41045, addFeeCreditCodec) .typecase(41046, currentFeeCreditCodec) // - .typecase(37000, spliceInitCodec) - .typecase(37002, spliceAckCodec) - .typecase(37004, spliceLockedCodec) + .typecase(37000, experimentalSpliceInitCodec) + .typecase(37002, experimentalSpliceAckCodec) + .typecase(37004, experimentalSpliceLockedCodec) // // diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 11034b42d0..dd2d66d0f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -93,11 +93,16 @@ case class TxAddInput(channelId: ByteVector32, sequence: Long, tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId { val sharedInput_opt: Option[OutPoint] = tlvStream.get[TxAddInputTlv.SharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput)) + .orElse(tlvStream.get[TxAddInputTlv.ExperimentalSharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput))) } object TxAddInput { def apply(channelId: ByteVector32, serialId: UInt64, sharedInput: OutPoint, sequence: Long): TxAddInput = { - TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(TxAddInputTlv.SharedInputTxId(sharedInput.txid))) + val tlvs = Set[TxAddInputTlv]( + TxAddInputTlv.SharedInputTxId(sharedInput.txid), + TxAddInputTlv.ExperimentalSharedInputTxId(sharedInput.txid), + ) + TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(tlvs)) } } @@ -123,11 +128,16 @@ case class TxSignatures(channelId: ByteVector32, witnesses: Seq[ScriptWitness], tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig) + .orElse(tlvStream.get[TxSignaturesTlv.ExperimentalPreviousFundingTxSig].map(_.sig)) } object TxSignatures { def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = { - TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv])) + val tlvs = Set( + previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig), + previousFundingSig_opt.map(TxSignaturesTlv.ExperimentalPreviousFundingTxSig), + ).flatten[TxSignaturesTlv] + TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } } @@ -327,6 +337,19 @@ object SpliceInit { } } +case class ExperimentalSpliceInit(channelId: ByteVector32, + fundingContribution: Satoshi, + feerate: FeeratePerKw, + lockTime: Long, + fundingPubKey: PublicKey, + tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceInit(): SpliceInit = SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, tlvStream) +} + +object ExperimentalSpliceInit { + def from(msg: SpliceInit): ExperimentalSpliceInit = ExperimentalSpliceInit(msg.channelId, msg.fundingContribution, msg.feerate, msg.lockTime, msg.fundingPubKey, msg.tlvStream) +} + case class SpliceAck(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, @@ -348,11 +371,32 @@ object SpliceAck { } } +case class ExperimentalSpliceAck(channelId: ByteVector32, + fundingContribution: Satoshi, + fundingPubKey: PublicKey, + tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceAck(): SpliceAck = SpliceAck(channelId, fundingContribution, fundingPubKey, tlvStream) +} + +object ExperimentalSpliceAck { + def from(msg: SpliceAck): ExperimentalSpliceAck = ExperimentalSpliceAck(msg.channelId, msg.fundingContribution, msg.fundingPubKey, msg.tlvStream) +} + case class SpliceLocked(channelId: ByteVector32, fundingTxId: TxId, tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { } +case class ExperimentalSpliceLocked(channelId: ByteVector32, + fundingTxId: TxId, + tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceLocked(): SpliceLocked = SpliceLocked(channelId, fundingTxId, tlvStream) +} + +object ExperimentalSpliceLocked { + def from(msg: SpliceLocked): ExperimentalSpliceLocked = ExperimentalSpliceLocked(msg.channelId, msg.fundingTxId, msg.tlvStream) +} + case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent @@ -431,7 +475,8 @@ case class CommitSig(channelId: ByteVector32, signature: ByteVector64, htlcSignatures: List[ByteVector64], tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { - val batchSize: Int = tlvStream.get[CommitSigTlv.BatchTlv].map(_.size).getOrElse(1) + val fundingTxId_opt: Option[TxId] = tlvStream.get[CommitSigTlv.BatchTlv].map(_.fundingTxId) + val batchSize: Int = tlvStream.get[CommitSigTlv.BatchTlv].map(_.size).orElse(tlvStream.get[CommitSigTlv.ExperimentalBatchTlv].map(_.size)).getOrElse(1) } case class RevokeAndAck(channelId: ByteVector32, diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json index 7e8e183d49..965cbc795c 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json @@ -20,7 +20,6 @@ "initFeatures" : { "activated" : { "option_route_blinding" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -32,7 +31,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "remoteParams" : { @@ -52,7 +51,6 @@ "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -62,7 +60,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json index c190f0a5be..80cbccc6d0 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json @@ -22,7 +22,6 @@ "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -32,7 +31,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "remoteParams" : { @@ -50,7 +49,6 @@ "initFeatures" : { "activated" : { "option_route_blinding" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -62,7 +60,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json index 8ef01bebf6..109c799b6b 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json @@ -19,7 +19,6 @@ "activated" : { "option_route_blinding" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_anchor_outputs" : "optional", @@ -32,7 +31,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "remoteParams" : { @@ -51,7 +50,6 @@ "option_route_blinding" : "optional", "option_provide_storage" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_anchor_outputs" : "optional", @@ -64,7 +62,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json index 1d8b1d5a7e..63b3dbc555 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json @@ -20,7 +20,6 @@ "option_route_blinding" : "optional", "option_provide_storage" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_anchor_outputs" : "optional", @@ -33,7 +32,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "remoteParams" : { @@ -51,7 +50,6 @@ "activated" : { "option_route_blinding" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_anchor_outputs" : "optional", @@ -64,7 +62,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json index 40bc72d116..c81d9ac387 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json @@ -22,7 +22,6 @@ "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -32,7 +31,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "remoteParams" : { @@ -50,7 +49,6 @@ "initFeatures" : { "activated" : { "option_route_blinding" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -62,7 +60,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json index 29fb6d0eb3..e64f71d68b 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json @@ -20,7 +20,6 @@ "activated" : { "option_route_blinding" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -32,7 +31,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 155 ] } }, "remoteParams" : { @@ -52,7 +51,6 @@ "option_route_blinding" : "optional", "option_provide_storage" : "optional", "option_dual_fund" : "optional", - "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", "option_quiesce" : "optional", @@ -62,7 +60,7 @@ "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ 155, 50001 ] } }, "channelFlags" : { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 9084e843dc..4a371e3a7a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -107,7 +107,7 @@ object TestConstants { Features.RouteBlinding -> FeatureSupport.Optional, Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.Quiescence -> FeatureSupport.Optional, - Features.SplicePrototype -> FeatureSupport.Optional, + Features.Splicing -> FeatureSupport.Optional, Features.ProvideStorage -> FeatureSupport.Optional, ), unknown = Set(UnknownFeature(TestFeature.optional)) @@ -292,7 +292,7 @@ object TestConstants { Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional, Features.Quiescence -> FeatureSupport.Optional, - Features.SplicePrototype -> FeatureSupport.Optional, + Features.Splicing -> FeatureSupport.Optional, ), pluginParams = Nil, overrideInitFeatures = Map.empty, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala index 32b19ea443..2afb271b1f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala @@ -25,9 +25,7 @@ class GossipIntegrationSpec extends FixtureSpec with IntegrationPatience { override def createFixture(testData: TestData): FixtureParam = { // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) - .modify(_.features).using(_.add(Features.SplicePrototype, FeatureSupport.Optional)) val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) - .modify(_.features).using(_.add(Features.SplicePrototype, FeatureSupport.Optional)) val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) ThreeNodesFixture(aliceParams, bobParams, carolParams, testData.name) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala index 55edb1e45c..7bcc61abcf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala @@ -126,7 +126,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory test("add liquidity if on-the-fly funding is used", Tag(noPlugin)) { f => import f._ - val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) + val features = defaultFeatures.add(Features.Splicing, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) val open = createOpenDualFundedChannelMessage().copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), @@ -249,7 +249,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory test("reject on-the-fly channel if another channel exists", Tag(noPlugin)) { f => import f._ - val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) + val features = defaultFeatures.add(Features.Splicing, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) val open = createOpenDualFundedChannelMessage().copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index fda08fff11..8ac45c378c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -19,10 +19,11 @@ package fr.acinq.eclair.io import akka.actor.PoisonPill import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, OutPoint, SatoshiLong, Transaction, TxId} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, PaymentSecret, StaticRemoteKey, VariableLengthOnion} +import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.ConnectionDown import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -496,5 +497,47 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi } } + test("convert experimental splice messages") { f => + import f._ + val remoteInit = protocol.Init(Bob.nodeParams.features.initFeatures().copy(unknown = Set(UnknownFeature(155)))) + connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit) + + val spliceInit = SpliceInit(randomBytes32(), 100_000 sat, FeeratePerKw(5000 sat), 0, randomKey().publicKey) + val spliceAck = SpliceAck(randomBytes32(), 50_000 sat, randomKey().publicKey) + val spliceLocked = SpliceLocked(randomBytes32(), TxId(randomBytes32())) + + // Outgoing messages use the experimental version of splicing. + peer.send(peerConnection, spliceInit) + transport.expectMsg(ExperimentalSpliceInit.from(spliceInit)) + peer.send(peerConnection, spliceAck) + transport.expectMsg(ExperimentalSpliceAck.from(spliceAck)) + peer.send(peerConnection, spliceLocked) + transport.expectMsg(ExperimentalSpliceLocked.from(spliceLocked)) + + // Incoming messages are converted from their experimental version. + transport.send(peerConnection, ExperimentalSpliceInit.from(spliceInit)) + peer.expectMsg(spliceInit) + transport.expectMsgType[TransportHandler.ReadAck] + transport.send(peerConnection, ExperimentalSpliceAck.from(spliceAck)) + peer.expectMsg(spliceAck) + transport.expectMsgType[TransportHandler.ReadAck] + transport.send(peerConnection, ExperimentalSpliceLocked.from(spliceLocked)) + peer.expectMsg(spliceLocked) + transport.expectMsgType[TransportHandler.ReadAck] + + // Incompatible TLVs are dropped when sending messages to peers using the experimental version. + val txAddInput = TxAddInput(randomBytes32(), UInt64(0), OutPoint(TxId(randomBytes32()), 3), 0) + assert(txAddInput.tlvStream.get[TxAddInputTlv.SharedInputTxId].nonEmpty) + peer.send(peerConnection, txAddInput) + assert(transport.expectMsgType[TxAddInput].tlvStream.get[TxAddInputTlv.SharedInputTxId].isEmpty) + val txSignatures = TxSignatures(randomBytes32(), Transaction(2, Nil, Nil, 0), Nil, Some(randomBytes64())) + assert(txSignatures.tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].nonEmpty) + peer.send(peerConnection, txSignatures) + assert(transport.expectMsgType[TxSignatures].tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].isEmpty) + val commitSig = CommitSig(randomBytes32(), randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2, TxId(randomBytes32())), CommitSigTlv.ExperimentalBatchTlv(2))) + peer.send(peerConnection, commitSig) + assert(transport.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.BatchTlv].isEmpty) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index fde6079213..ec6841dfc2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -49,13 +49,13 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional, Features.DualFunding -> FeatureSupport.Optional, - Features.SplicePrototype -> FeatureSupport.Optional, + Features.Splicing -> FeatureSupport.Optional, Features.OnTheFlyFunding -> FeatureSupport.Optional, ) val remoteFeaturesWithFeeCredit = Features( Features.DualFunding -> FeatureSupport.Optional, - Features.SplicePrototype -> FeatureSupport.Optional, + Features.Splicing -> FeatureSupport.Optional, Features.OnTheFlyFunding -> FeatureSupport.Optional, Features.FundingFeeCredit -> FeatureSupport.Optional, ) @@ -181,7 +181,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val nodeParams = TestConstants.Alice.nodeParams .modify(_.features.activated).using(_ + (Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional)) .modify(_.features.activated).using(_ + (Features.DualFunding -> FeatureSupport.Optional)) - .modify(_.features.activated).using(_ + (Features.SplicePrototype -> FeatureSupport.Optional)) + .modify(_.features.activated).using(_ + (Features.Splicing -> FeatureSupport.Optional)) .modify(_.features.activated).using(_ + (Features.OnTheFlyFunding -> FeatureSupport.Optional)) .modify(_.features.activated).usingIf(test.tags.contains(withFeeCredit))(_ + (Features.FundingFeeCredit -> FeatureSupport.Optional)) val remoteNodeId = randomKey().publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index ec318af73c..cbff63fb5d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -159,6 +159,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 07 bbbbbbbbbbbbbb" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, hex"bbbbbbbbbbbbbb")))), hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, signature, Nil), + hex"0084" ++ channelId ++ signature ++ hex"0000" ++ hex"00 22 0003" ++ txId.value.reverse -> CommitSig(channelId, signature, Nil, TlvStream(CommitSigTlv.BatchTlv(3, txId))), hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), @@ -205,7 +206,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", TxAddInput(channelId2, UInt64(0), Some(tx2), 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", TxAddInput(channelId1, UInt64(561), Some(tx1), 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", - TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106", + TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 00201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Set.empty[TxAddOutputTlv], Set(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", @@ -214,7 +215,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", - TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025940aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", @@ -390,28 +391,30 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val channelId = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val fundingTxId = TxId(TxHash(ByteVector32(hex"24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"))) val fundingPubkey = PublicKey(hex"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") - val fundingRate = LiquidityAds.FundingRate(100_000.sat, 100_000.sat, 400, 150, 0.sat, 0.sat) + val fundingRate = LiquidityAds.FundingRate(100_000 sat, 100_000 sat, 400, 150, 0 sat, 0 sat) val testCases = Seq( // @formatter:off - SpliceInit(channelId, 100_000 sat, FeeratePerKw(2500 sat), 100, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = false, None) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840", - SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000", - SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680", - SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceAck(channelId, (-25_000).sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200", - SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1", - SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566", + SpliceInit(channelId, 100_000 sat, FeeratePerKw(2500 sat), 100, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = false, None) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840", + SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = true, None) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0200 fe4700000704017d7840", + SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000", + SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680", + SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = true, None, None) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0200 fe4700000703989680", + SpliceAck(channelId, 0.sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceAck(channelId, (-25_000).sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200", + SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1", + SpliceLocked(channelId, fundingTxId) -> hex"004d aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566", // @formatter:on ) - testCases.foreach { case (message, bin) => + testCases.foreach { case (msg, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded == message) - val encoded = lightningMessageCodec.encode(message).require.bytes + assert(decoded == msg) + val encoded = lightningMessageCodec.encode(msg).require.bytes assert(encoded == bin) } }