diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 982403d640..e18144f1d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -439,6 +439,8 @@ sealed trait LocalFundingStatus { def signedTx_opt: Option[Transaction] /** We store local signatures for the purpose of retransmitting if the funding/splicing flow is interrupted. */ def localSigs_opt: Option[TxSignatures] + /** Basic information about the liquidity purchase negotiated in this transaction, if any. */ + def liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo] } object LocalFundingStatus { sealed trait NotLocked extends LocalFundingStatus @@ -453,15 +455,16 @@ object LocalFundingStatus { */ case class SingleFundedUnconfirmedFundingTx(signedTx_opt: Option[Transaction]) extends UnconfirmedFundingTx with NotLocked { override val localSigs_opt: Option[TxSignatures] = None + override val liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo] = None } - case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction, createdAt: BlockHeight, fundingParams: InteractiveTxParams) extends UnconfirmedFundingTx with NotLocked { + case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction, createdAt: BlockHeight, fundingParams: InteractiveTxParams, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with NotLocked { override val signedTx_opt: Option[Transaction] = sharedTx.signedTx_opt override val localSigs_opt: Option[TxSignatures] = Some(sharedTx.localSigs) } - case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures]) extends UnconfirmedFundingTx with Locked { + case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with Locked { override val signedTx_opt: Option[Transaction] = Some(tx) } - case class ConfirmedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures]) extends LocalFundingStatus with Locked { + case class ConfirmedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked { override val signedTx_opt: Option[Transaction] = Some(tx) } } 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 de33913209..d7d80575ea 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 @@ -1153,6 +1153,10 @@ case class Commitments(params: ChannelParams, def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = { all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt) } + + def liquidityPurchase(fundingTxId: TxId): Option[LiquidityAds.PurchaseBasicInfo] = { + all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.liquidityPurchase_opt) + } /** * Update the local/remote funding status 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 1ad6ad4f57..0383bb96eb 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 @@ -1105,7 +1105,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(msg: TxSignatures, d: DATA_NORMAL) => d.commitments.latest.localFundingStatus match { - case dfu@LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _) if fundingTx.txId == msg.txId => + case dfu@LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) if fundingTx.txId == msg.txId => // we already sent our tx_signatures InteractiveTxSigningSession.addRemoteSigs(keyManager, d.commitments.params, dfu.fundingParams, fundingTx, msg) match { case Left(cause) => @@ -1149,7 +1149,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(w: WatchPublishedTriggered, d: DATA_NORMAL) => - val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid)) + val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match { case Right((commitments1, _)) => watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks), delay_opt = None) @@ -1910,7 +1910,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case d: DATA_NORMAL => d.spliceStatus match { case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) case _ => d.commitments.latest.localFundingStatus match { - case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId)) + case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId)) case _ => Set.empty } } @@ -2300,11 +2300,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // slightly before us. In that case, the WatchConfirmed may trigger first, and it would be inefficient to let the // WatchPublished override our funding status: it will make us set a new WatchConfirmed that will instantly // trigger and rewrite the funding status again. - val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case LocalFundingStatus.ConfirmedFundingTx(tx, _) => tx }.exists(_.txid == w.tx.txid) + val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case LocalFundingStatus.ConfirmedFundingTx(tx, _, _) => tx }.exists(_.txid == w.tx.txid) if (alreadyConfirmed) { stay() } else { - val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid)) + val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match { case Right((commitments1, _)) => log.info(s"zero-conf funding txid=${w.tx.txid} has been published") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 46daea5e66..4e89d3a1df 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -712,7 +712,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId) - val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid)) + val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match { case Right((commitments1, _)) => // we still watch the funding tx for confirmation even if we can use the zero-conf channel right away diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 4eaf218f91..7cd37bca2f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -391,7 +391,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, None) + val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, None, None) d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match { case Right((commitments1, _)) => log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index da66aa0253..7dbb338bab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -82,7 +82,7 @@ trait CommonFundingHandlers extends CommonHandlers { } case _ => () // in the dual-funding case, we have already verified the funding tx } - val fundingStatus = ConfirmedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid)) + val fundingStatus = ConfirmedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) // When a splice transaction confirms, it double-spends all the commitment transactions that only applied to the // previous funding transaction. Our peer cannot publish the corresponding revoked commitments anymore, so we can 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 0b78f2de31..06afc201ea 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 @@ -865,7 +865,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon ) context.system.eventStream ! EventStream.Publish(ChannelLiquidityPurchased(replyTo.toClassic, channelParams.channelId, remoteNodeId, purchase)) } - replyTo ! Succeeded(InteractiveTxSigningSession.WaitingForSigs(fundingParams, purpose.fundingTxIndex, signedTx, Left(localCommit), remoteCommit), commitSig, liquidityPurchase_opt) + val signingSession = InteractiveTxSigningSession.WaitingForSigs( + fundingParams, + purpose.fundingTxIndex, + signedTx, + Left(localCommit), + remoteCommit, + liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator)) + ) + replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt) Behaviors.stopped case WalletFailure(t) => log.error("could not sign funding transaction: ", t) @@ -1050,7 +1058,8 @@ object InteractiveTxSigningSession { fundingTxIndex: Long, fundingTx: PartiallySignedSharedTransaction, localCommit: Either[UnsignedLocalCommit, LocalCommit], - remoteCommit: RemoteCommit) extends InteractiveTxSigningSession { + remoteCommit: RemoteCommit, + liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input) val localCommitIndex: Long = localCommit.fold(_.index, _.index) @@ -1061,7 +1070,7 @@ object InteractiveTxSigningSession { val localPerCommitmentPoint = nodeParams.channelKeyManager.commitmentPoint(channelKeyPath, localCommitIndex) LocalCommit.fromCommitSig(nodeParams.channelKeyManager, channelParams, fundingTx.txId, fundingTxIndex, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, localPerCommitmentPoint).map { signedLocalCommit => if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { - val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams) + val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt) val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) SendingSigs(fundingStatus, commitment, fundingTx.localSigs) } else { @@ -1086,7 +1095,7 @@ object InteractiveTxSigningSession { Left(f) case Right(fullySignedTx) => log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length) - val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, nodeParams.currentBlockHeight, fundingParams) + val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt) val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index ec4fbe1326..36b7e93961 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -14,7 +14,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol.{TxSignatures, UpdateAddHtlc, UpdateMessage} +import fr.acinq.eclair.wire.protocol.{LiquidityAds, TxSignatures, UpdateAddHtlc, UpdateMessage} import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature, channel} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -382,10 +382,27 @@ private[channel] object ChannelCodecs4 { .typecase(0x01, partiallySignedSharedTransactionCodec) .typecase(0x02, fullySignedSharedTransactionCodec) + private val liquidityFeesCodec: Codec[LiquidityAds.Fees] = (("miningFees" | satoshi) :: ("serviceFees" | satoshi)).as[LiquidityAds.Fees] + + private val liquidityPurchaseCodec: Codec[LiquidityAds.PurchaseBasicInfo] = ( + ("isBuyer" | bool8) :: + ("amount" | satoshi) :: + ("fees" | liquidityFeesCodec)).as[LiquidityAds.PurchaseBasicInfo] + + private val dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec: Codec[DualFundedUnconfirmedFundingTx] = ( + ("sharedTx" | signedSharedTransactionCodec) :: + ("createdAt" | blockHeight) :: + ("fundingParams" | fundingParamsCodec) :: + ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[DualFundedUnconfirmedFundingTx].xmap( + dfu => fillSharedInputScript(dfu), + dfu => dfu + ) + private val dualFundedUnconfirmedFundingTxCodec: Codec[DualFundedUnconfirmedFundingTx] = ( ("sharedTx" | signedSharedTransactionCodec) :: ("createdAt" | blockHeight) :: - ("fundingParams" | fundingParamsCodec)).as[DualFundedUnconfirmedFundingTx].xmap( + ("fundingParams" | fundingParamsCodec) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[DualFundedUnconfirmedFundingTx].xmap( dfu => fillSharedInputScript(dfu), dfu => dfu ) @@ -407,11 +424,14 @@ private[channel] object ChannelCodecs4 { val fundingTxStatusCodec: Codec[LocalFundingStatus] = discriminated[LocalFundingStatus].by(uint8) .typecase(0x01, optional(bool8, txCodec).as[SingleFundedUnconfirmedFundingTx]) - .typecase(0x02, dualFundedUnconfirmedFundingTxCodec) - .typecase(0x05, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec))).as[ZeroconfPublishedFundingTx]) - .typecase(0x06, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec))).as[ConfirmedFundingTx]) - .typecase(0x03, (txCodec :: provide(Option.empty[TxSignatures])).as[ZeroconfPublishedFundingTx]) - .typecase(0x04, (txCodec :: provide(Option.empty[TxSignatures])).as[ConfirmedFundingTx]) + .typecase(0x07, dualFundedUnconfirmedFundingTxCodec) + .typecase(0x08, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ZeroconfPublishedFundingTx]) + .typecase(0x09, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ConfirmedFundingTx]) + .typecase(0x02, dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec) + .typecase(0x05, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) + .typecase(0x06, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) + .typecase(0x03, (txCodec :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) + .typecase(0x04, (txCodec :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) val remoteFundingStatusCodec: Codec[RemoteFundingStatus] = discriminated[RemoteFundingStatus].by(uint8) .typecase(0x01, provide(RemoteFundingStatus.NotLocked)) @@ -590,30 +610,41 @@ private[channel] object ChannelCodecs4 { // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel // cannot be used for payments. - private val interactiveTxWaitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = { + private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTx" | commitTxCodec) :: ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[UnsignedLocalCommit] + val waitingForSigsWithoutLiquidityPurchaseCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( + ("fundingParams" | fundingParamsCodec) :: + ("fundingTxIndex" | uint32) :: + ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[InteractiveTxSigningSession.WaitingForSigs] + val waitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: ("fundingTxIndex" | uint32) :: ("fundingTx" | partiallySignedSharedTransactionCodec) :: ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: - ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec))).as[InteractiveTxSigningSession.WaitingForSigs] + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] - waitingForSigsCodec + (waitingForSigsWithoutLiquidityPurchaseCodec, waitingForSigsCodec) } val rbfStatusCodec: Codec[RbfStatus] = discriminated[RbfStatus].by(uint8) .\(0x01) { case status: RbfStatus if !status.isInstanceOf[RbfStatus.RbfWaitingForSigs] => RbfStatus.NoRbf }(provide(RbfStatus.NoRbf)) - .\(0x02) { case status: RbfStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[RbfStatus.RbfWaitingForSigs]) + .\(0x03) { case status: RbfStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[RbfStatus.RbfWaitingForSigs]) + .\(0x02) { case status: RbfStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[RbfStatus.RbfWaitingForSigs]) val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) - .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: @@ -640,9 +671,17 @@ private[channel] object ChannelCodecs4 { ("secondRemotePerCommitmentPoint" | publicKey) :: ("localPushAmount" | millisatoshi) :: ("remotePushAmount" | millisatoshi) :: - ("status" | interactiveTxWaitingForSigsCodec) :: + ("status" | interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec) :: ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( + ("channelParams" | paramsCodec) :: + ("secondRemotePerCommitmentPoint" | publicKey) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("status" | interactiveTxWaitingForSigsCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: ("localPushAmount" | millisatoshi) :: @@ -750,6 +789,7 @@ private[channel] object ChannelCodecs4 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x13, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec) .typecase(0x12, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec) .typecase(0x11, Codecs.DATA_CLOSING_11_Codec) .typecase(0x10, Codecs.DATA_NEGOTIATING_10_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala index 933e3090fa..c3d1f252d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala @@ -225,6 +225,7 @@ object LiquidityAds { def amount: Satoshi def fees: Fees def paymentDetails: PaymentDetails + def basicInfo(isBuyer: Boolean): PurchaseBasicInfo = PurchaseBasicInfo(isBuyer, amount, fees) // @formatter:on } @@ -238,6 +239,9 @@ object LiquidityAds { case class WillFundPurchase(willFund: WillFund, purchase: Purchase) + /** Minimal information about a liquidity purchase. */ + case class PurchaseBasicInfo(isBuyer: Boolean, amount: Satoshi, fees: Fees) + object Codecs { val fundingRate: Codec[FundingRate] = ( ("minAmount" | satoshi32) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index ec58b9cc3c..341584d6d1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -1829,7 +1829,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fwdSplice.forwardBob2Alice[TxComplete] val successA2 = alice2bob.expectMsgType[Succeeded] + assert(successA2.signingSession.liquidityPurchase_opt.contains(purchase.basicInfo(isBuyer = true))) val successB2 = bob2alice.expectMsgType[Succeeded] + assert(successB2.signingSession.liquidityPurchase_opt.contains(purchase.basicInfo(isBuyer = false))) val (spliceTxA1, commitmentA2, _, commitmentB2) = fixtureParams.exchangeSigsBobFirst(fundingParamsB1, successA2, successB2) assert(commitmentA2.localCommit.spec.toLocal == commitmentA1.localCommit.spec.toLocal - spliceFeeA - purchase.fees.total) assert(commitmentB2.localCommit.spec.toLocal == commitmentB1.localCommit.spec.toLocal + fundingB + purchase.fees.total) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index 3c2180c679..6ca3b27bcc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -17,7 +17,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, Scripts} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.Codecs._ import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.channelDataCodec -import fr.acinq.eclair.wire.protocol.TxSignatures +import fr.acinq.eclair.wire.protocol.{LiquidityAds, TxSignatures} import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, UInt64, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -141,13 +141,15 @@ class ChannelCodecs4Spec extends AnyFunSuite { fundingTxIndex = 0, PartiallySignedSharedTransaction(fundingTx, TxSignatures(channelId, randomTxId(), Nil)), Left(UnsignedLocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 100_000_000 msat, 75_000_000 msat), commitTx, Nil)), - RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 75_000_000 msat, 100_000_000 msat), randomTxId(), randomKey().publicKey) + RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 75_000_000 msat, 100_000_000 msat), randomTxId(), randomKey().publicKey), + Some(LiquidityAds.PurchaseBasicInfo(isBuyer = true, 100_000 sat, LiquidityAds.Fees(1000 sat, 500 sat))), ) val testCases = Map( RbfStatus.NoRbf -> RbfStatus.NoRbf, RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 100_000.sat, 0, None)) -> RbfStatus.NoRbf, RbfStatus.RbfInProgress(None, null, None) -> RbfStatus.NoRbf, RbfStatus.RbfWaitingForSigs(waitingForSigs) -> RbfStatus.RbfWaitingForSigs(waitingForSigs), + RbfStatus.RbfWaitingForSigs(waitingForSigs.copy(liquidityPurchase_opt = None)) -> RbfStatus.RbfWaitingForSigs(waitingForSigs.copy(liquidityPurchase_opt = None)), RbfStatus.RbfAborted -> RbfStatus.NoRbf, ) testCases.foreach { case (status, expected) => @@ -184,8 +186,14 @@ class ChannelCodecs4Spec extends AnyFunSuite { )), remoteFundingPubKey = PrivateKey(ByteVector.fromValidHex("01" * 32)).publicKey, localOutputs = Nil, lockTime = 0, dustLimit = 330.sat, targetFeerate = FeeratePerKw(FeeratePerByte(3.sat)), requireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)), + liquidityPurchase_opt = None ) assert(decoded == dualFundedUnconfirmedFundingTx) + + val dualFundedUnconfirmedFundingTx1 = dualFundedUnconfirmedFundingTx.copy( + liquidityPurchase_opt = Some(LiquidityAds.PurchaseBasicInfo(isBuyer = true, 250_000 sat, LiquidityAds.Fees(1500 sat, 700 sat))) + ) + assert(fundingTxStatusCodec.decode(fundingTxStatusCodec.encode(dualFundedUnconfirmedFundingTx1).require).require.value == dualFundedUnconfirmedFundingTx1) } test("decode local params pay commit tx fees field") {