diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 6d5ece69ac..d43088c421 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,7 +4,38 @@ ## Major changes - +### 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 diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index aecf2c25cc..345f7b7cc0 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -84,6 +84,7 @@ eclair { // node that you trust using override-init-features (see below). option_zeroconf = disabled keysend = disabled + 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 b668ff62b7..2d1ddaea19 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 } @@ -305,6 +304,11 @@ object Features { val mandatory = 54 } + case object Splicing extends Feature with InitFeature with NodeFeature { + val rfcName = "option_splice" + val mandatory = 62 + } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // We're not advertising these bits yet in our announcements, clients have to assume support. // This is why we haven't added them yet to `areSupported`. @@ -322,12 +326,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. @@ -369,9 +367,9 @@ object Features { PaymentMetadata, ZeroConf, KeySend, + Splicing, TrampolinePaymentPrototype, AsyncPaymentPrototype, - SplicePrototype, OnTheFlyFunding, FundingFeeCredit ) @@ -387,7 +385,6 @@ object Features { TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: 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 f2e2e72c0d..24dcdb9a2f 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 @@ -680,9 +680,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) } @@ -1076,8 +1078,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 9ea228160b..453189317d 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 @@ -852,7 +852,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() @@ -2108,7 +2108,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, @@ -2123,6 +2123,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)) @@ -2142,7 +2155,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, @@ -2183,8 +2196,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) @@ -2195,20 +2209,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. @@ -2218,14 +2237,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 { @@ -2250,23 +2286,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. @@ -2771,7 +2810,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/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 836ceb9d44..053c21a3ea 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/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 77e2d4dcec..c789db7779 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 @@ -418,17 +418,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] @@ -508,6 +527,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) @@ -539,9 +561,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 080fe906fe..66d2a25d30 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)) } } @@ -325,6 +335,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, @@ -346,11 +369,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 @@ -417,7 +461,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 d9ac1ad8f4..f5e406dd80 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)) @@ -289,7 +289,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/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 b3ba0346d9..c1d584a14c 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 @@ -1719,7 +1719,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik awaitCond(bob.stateName == OFFLINE) } - private def reconnect(f: FixtureParam, sendReestablish: Boolean = true): (ChannelReestablish, ChannelReestablish) = { + private def reconnect(f: FixtureParam): (ChannelReestablish, ChannelReestablish) = { import f._ val aliceInit = Init(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures) @@ -1727,9 +1727,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] - if (sendReestablish) alice2bob.forward(bob) + alice2bob.forward(bob) val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] - if (sendReestablish) bob2alice.forward(alice) + bob2alice.forward(alice) (channelReestablishAlice, channelReestablishBob) } @@ -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(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) 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(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] 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 1911f21c44..6ffd184919 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 3f6acd587f..8325920f43 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), @@ -236,7 +236,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 02cdcbe347..3bd04c98ad 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 @@ -157,6 +157,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")))), @@ -203,7 +204,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", @@ -212,7 +213,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", @@ -388,28 +389,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) } }