diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index ced11215ce..99d2d49fa9 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -6,6 +6,39 @@ +### 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. + ### Package relay With Bitcoin Core 28.1, eclair starts relying on the `submitpackage` RPC during channel force-close. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 5fe58f38db..7cae7a0ad4 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -88,6 +88,7 @@ eclair { option_zeroconf = disabled keysend = disabled option_simple_close=optional + option_splice = optional trampoline_payment_prototype = disabled async_payment_prototype = disabled on_the_fly_funding = disabled @@ -109,7 +110,7 @@ eclair { funding { // Each RBF attempt adds more data that we need to store and process, so we want to limit our peers to a reasonable use of RBF. remote-rbf-limits { - max-attempts = 5 // maximum number of RBF attempts our peer is allowed to make + max-attempts = 10 // maximum number of RBF attempts our peer is allowed to make attempt-delta-blocks = 6 // minimum number of blocks between RBF attempts } // Duration after which we abort a channel creation. If our peer seems unresponsive and doesn't complete the 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 e2cf36d2a1..6d0f787d38 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 } @@ -314,6 +313,11 @@ object Features { val mandatory = 60 } + case object Splicing extends Feature with InitFeature with NodeFeature { + val rfcName = "option_splice" + val mandatory = 62 + } + /** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */ case object WakeUpNotificationClient extends Feature with InitFeature { val rfcName = "wake_up_notification_client" @@ -337,12 +341,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. @@ -386,10 +384,10 @@ object Features { ZeroConf, KeySend, SimpleClose, + Splicing, WakeUpNotificationClient, TrampolinePaymentPrototype, AsyncPaymentPrototype, - SplicePrototype, OnTheFlyFunding, FundingFeeCredit ) @@ -406,7 +404,6 @@ object Features { KeySend -> (VariableLengthOnion :: Nil), SimpleClose -> (ShutdownAnySegwit :: Nil), AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil), - OnTheFlyFunding -> (SplicePrototype :: Nil), FundingFeeCredit -> (OnTheFlyFunding :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 66bc94f722..a7deebf73a 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 @@ -231,7 +231,7 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePer params.commitmentFormat match { case _: SegwitV0CommitmentFormat => val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig - CommitSig(params.channelId, sig, htlcSigs.toList) + CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(CommitSigTlv.FundingTx(commitInput.outPoint.txid))) case _: SimpleTaprootChannelCommitmentFormat => ??? } } @@ -658,7 +658,8 @@ case class Commitment(fundingTxIndex: Long, Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) val tlvs = Set( - if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None + Some(CommitSigTlv.FundingTx(fundingTxId)), + if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None, ).flatten[CommitSigTlv] val commitSig = params.commitmentFormat match { case _: SegwitV0CommitmentFormat => @@ -1042,8 +1043,10 @@ case class Commitments(params: ChannelParams, case commitSig: CommitSig => Seq(commitSig) } val commitKeys = LocalCommitmentKeys(params, channelKeys, 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(sigs).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 = sigs.find(_.fundingTxId_opt.contains(commitment.fundingTxId)).getOrElse(sigs(idx)) commitment.receiveCommit(params, channelKeys, commitKeys, changes, 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 5a82f46aef..9f167240af 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 @@ -883,7 +883,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } 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() @@ -2327,7 +2327,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } case _ => Set.empty } - val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype)) { + val remoteFeatures = d.commitments.params.remoteParams.initFeatures + val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (remoteFeatures.hasFeature(Features.Splicing) || remoteFeatures.unknown.contains(UnknownFeature(154)) || remoteFeatures.unknown.contains(UnknownFeature(155))) { d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++ d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet } else Set.empty @@ -2456,7 +2457,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // We only send channel_ready for initial funding transactions. case Some(c) if c.fundingTxIndex != 0 => () case Some(c) => - val remoteSpliceSupport = d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype) + val remoteFeatures = d.commitments.params.remoteParams.initFeatures + val remoteSpliceSupport = remoteFeatures.hasFeature(Features.Splicing) || remoteFeatures.unknown.contains(UnknownFeature(154)) || remoteFeatures.unknown.contains(UnknownFeature(155)) // If our peer has not received our channel_ready, we retransmit it. val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node 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 8b18e06b6a..f089349fda 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 @@ -855,7 +855,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case _: SegwitV0CommitmentFormat => val localSigOfRemoteTx = remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig val htlcSignatures = sortedHtlcTxs.map(_.sign(remoteCommitmentKeys, channelParams.commitmentFormat)).toList - val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) + val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, TlvStream(CommitSigTlv.FundingTx(fundingTx.txid))) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) 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 8f2fe0254c..967a939e94 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 @@ -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, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond, UnknownFeature} import scodec.Attempt import scodec.bits.ByteVector @@ -206,9 +206,19 @@ 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 + val useExperimentalSplice = d.remoteInit.features.unknown.contains(UnknownFeature(154)) || d.remoteInit.features.unknown.contains(UnknownFeature(155)) msg match { - case batch: CommitSigBatch => batch.messages.foreach(msg => d.transport forward msg) - case msg => d.transport forward msg + // 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: TxSignatures if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxSignaturesTlv.PreviousFundingTxSig]))) + case batch: CommitSigBatch if useExperimentalSplice => batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.FundingTx])))) + case batch: CommitSigBatch => + d.transport forward StartBatch.commitSigBatch(batch.channelId, batch.batchSize) + batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.ExperimentalBatchTlv])))) + case _ => d.transport forward msg } msg match { // If we send any channel management message to this peer, the connection should be persistent. @@ -348,8 +358,51 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A // We immediately forward messages to the peer, unless they are part of a batch, in which case we wait to // receive the whole batch before forwarding. msg match { + case msg: StartBatch => + if (!msg.messageType_opt.contains(132)) { + log.debug("ignoring start_batch: we only support batching commit_sig messages") + d.transport ! Warning(msg.channelId, "invalid start_batch message: we only support batching commit_sig messages") + stay() + } else if (msg.batchSize > 20) { + log.debug("ignoring start_batch with batch_size = {} > 20", msg.batchSize) + d.transport ! Warning(msg.channelId, "invalid start_batch message: batch_size must not be greater than 20") + stay() + } else { + log.debug("starting commit_sig batch of size {} for channel_id={}", msg.batchSize, msg.channelId) + d.commitSigBatch_opt match { + case Some(pending) if pending.received.nonEmpty => + log.warning("starting batch with incomplete previous batch ({}/{} received)", pending.received.size, pending.batchSize) + // This is a spec violation from our peer: this will likely lead to a force-close. + d.transport ! Warning(msg.channelId, "invalid start_batch message: the previous batch is not done yet") + d.peer ! CommitSigBatch(pending.received) + case _ => () + } + stay() using d.copy(commitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, msg.batchSize, Nil))) + } + case msg: HasChannelId if d.commitSigBatch_opt.nonEmpty => + // We only support batches of commit_sig messages: other messages will simply be relayed individually. + val pending = d.commitSigBatch_opt.get + msg match { + case msg: CommitSig if msg.channelId == pending.channelId => + val received1 = pending.received :+ msg + if (received1.size == pending.batchSize) { + log.debug("received last commit_sig in batch for channel_id={}", msg.channelId) + d.peer ! CommitSigBatch(received1) + stay() using d.copy(commitSigBatch_opt = None) + } else { + log.debug("received commit_sig {}/{} in batch for channel_id={}", received1.size, pending.batchSize, msg.channelId) + stay() using d.copy(commitSigBatch_opt = Some(pending.copy(received = received1))) + } + case _ => + log.warning("received {} as part of a batch: we don't support batching that kind of messages", msg.getClass.getSimpleName) + if (pending.received.nonEmpty) d.peer ! CommitSigBatch(pending.received) + d.peer ! msg + stay() using d.copy(commitSigBatch_opt = None) + } case msg: CommitSig => - msg.tlvStream.get[CommitSigTlv.BatchTlv].map(_.size) match { + // We keep supporting the experimental version of splicing that older Phoenix wallets use. + // Once we're confident that enough Phoenix users have upgraded, we should remove this branch. + msg.tlvStream.get[CommitSigTlv.ExperimentalBatchTlv].map(_.size) match { case Some(batchSize) if batchSize > 25 => log.warning("received legacy batch of commit_sig exceeding our threshold ({} > 25), processing messages individually", batchSize) // We don't want peers to be able to exhaust our memory by sending batches of dummy messages that we keep in RAM. @@ -381,6 +434,16 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.peer ! msg stay() } + // If our peer is using the experimental splice version, we convert splice messages. + case msg: ExperimentalSpliceInit => + d.peer ! msg.toSpliceInit() + stay() + case msg: ExperimentalSpliceAck => + d.peer ! msg.toSpliceAck() + stay() + case msg: ExperimentalSpliceLocked => + d.peer ! msg.toSpliceLocked() + stay() case _ => d.peer ! msg stay() @@ -613,6 +676,7 @@ object PeerConnection { gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None, + commitSigBatch_opt: Option[PendingCommitSigBatch] = None, legacyCommitSigBatch_opt: Option[PendingCommitSigBatch] = None, isPersistent: Boolean) extends Data with HasTransport diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 7b561d3fa1..44d401aeb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -234,8 +234,16 @@ sealed trait ChannelReestablishTlv extends Tlv object ChannelReestablishTlv { + /** + * When disconnected in the middle of an interactive-tx session, this field is used to request a retransmission of + * [[TxSignatures]] for the given [[txId]]. + */ case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv + + /** The txid of the last [[ChannelReady]] or [[SpliceLocked]] message received before disconnecting, if any. */ case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv + + /** The txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel. */ case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv object NextFundingTlv { @@ -245,6 +253,7 @@ object ChannelReestablishTlv { object YourLastFundingLockedTlv { val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash) } + object MyCurrentFundingLockedTlv { val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash) } @@ -301,3 +310,14 @@ object ClosingTlv { ) } + +sealed trait StartBatchTlv extends Tlv + +object StartBatchTlv { + /** Type of [[LightningMessage]] that is included in the batch, when batching a single message type. */ + case class MessageType(tag: Int) extends StartBatchTlv + + val startBatchTlvCodec: Codec[TlvStream[StartBatchTlv]] = tlvStream(discriminated[StartBatchTlv].by(varint) + .typecase(UInt64(1), tlvField(uint16.as[MessageType])) + ) +} \ No newline at end of file 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 716d2a124e..0f84526f3a 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.crypto.Sphinx import fr.acinq.eclair.wire.protocol.CommonCodecs._ @@ -80,16 +81,27 @@ 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: this field maps it to the corresponding funding transaction. + * + * @param txId the funding transaction spent by this commitment. + */ + case class FundingTx(txId: TxId) extends CommitSigTlv - /** @param size the number of [[CommitSig]] messages in the batch */ - case class BatchTlv(size: Int) extends CommitSigTlv + private val fundingTxTlv: Codec[FundingTx] = tlvField(txIdAsHash) - object BatchTlv { - val codec: Codec[BatchTlv] = tlvField(tu16) - } + /** + * The experimental version of splicing included the number of [[CommitSig]] messages in the batch. + * This TLV can be removed once Phoenix users have upgraded to the official 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(1), fundingTxTlv) + .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 cd9f4da73f..5660b49692 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 @@ -243,6 +243,11 @@ object LightningMessageCodecs { ("lockTime" | uint32) :: ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingSig] + val startBatchCodec: Codec[StartBatch] = ( + ("channelId" | bytes32) :: + ("batchSize" | uint16) :: + ("tlvStream" | StartBatchTlv.startBatchTlvCodec)).as[StartBatch] + val updateAddHtlcCodec: Codec[UpdateAddHtlc] = ( ("channelId" | bytes32) :: ("id" | uint64overflow) :: @@ -434,17 +439,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] @@ -527,6 +551,10 @@ object LightningMessageCodecs { .typecase(72, txInitRbfCodec) .typecase(73, txAckRbfCodec) .typecase(74, txAbortCodec) + .typecase(77, spliceLockedCodec) + .typecase(80, spliceInitCodec) + .typecase(81, spliceAckCodec) + .typecase(127, startBatchCodec) .typecase(128, updateAddHtlcCodec) .typecase(130, updateFulfillHtlcCodec) .typecase(131, updateFailHtlcCodec) @@ -558,9 +586,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 885438d8a2..f17618410a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -93,11 +93,16 @@ case class TxAddInput(channelId: ByteVector32, sequence: Long, tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId { val sharedInput_opt: Option[OutPoint] = tlvStream.get[TxAddInputTlv.SharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput)) + .orElse(tlvStream.get[TxAddInputTlv.ExperimentalSharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput))) } object TxAddInput { def apply(channelId: ByteVector32, serialId: UInt64, sharedInput: OutPoint, sequence: Long): TxAddInput = { - TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(TxAddInputTlv.SharedInputTxId(sharedInput.txid))) + val tlvs = Set[TxAddInputTlv]( + TxAddInputTlv.SharedInputTxId(sharedInput.txid), + TxAddInputTlv.ExperimentalSharedInputTxId(sharedInput.txid), + ) + TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(tlvs)) } } @@ -123,11 +128,16 @@ case class TxSignatures(channelId: ByteVector32, witnesses: Seq[ScriptWitness], tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig) + .orElse(tlvStream.get[TxSignaturesTlv.ExperimentalPreviousFundingTxSig].map(_.sig)) } object TxSignatures { def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = { - TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv])) + val tlvs = Set( + previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig), + previousFundingSig_opt.map(TxSignaturesTlv.ExperimentalPreviousFundingTxSig), + ).flatten[TxSignaturesTlv] + TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } } @@ -327,6 +337,19 @@ object SpliceInit { } } +case class ExperimentalSpliceInit(channelId: ByteVector32, + fundingContribution: Satoshi, + feerate: FeeratePerKw, + lockTime: Long, + fundingPubKey: PublicKey, + tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceInit(): SpliceInit = SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, tlvStream) +} + +object ExperimentalSpliceInit { + def from(msg: SpliceInit): ExperimentalSpliceInit = ExperimentalSpliceInit(msg.channelId, msg.fundingContribution, msg.feerate, msg.lockTime, msg.fundingPubKey, msg.tlvStream) +} + case class SpliceAck(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, @@ -348,11 +371,32 @@ object SpliceAck { } } +case class ExperimentalSpliceAck(channelId: ByteVector32, + fundingContribution: Satoshi, + fundingPubKey: PublicKey, + tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceAck(): SpliceAck = SpliceAck(channelId, fundingContribution, fundingPubKey, tlvStream) +} + +object ExperimentalSpliceAck { + def from(msg: SpliceAck): ExperimentalSpliceAck = ExperimentalSpliceAck(msg.channelId, msg.fundingContribution, msg.fundingPubKey, msg.tlvStream) +} + case class SpliceLocked(channelId: ByteVector32, fundingTxId: TxId, tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { } +case class ExperimentalSpliceLocked(channelId: ByteVector32, + fundingTxId: TxId, + tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + def toSpliceLocked(): SpliceLocked = SpliceLocked(channelId, fundingTxId, tlvStream) +} + +object ExperimentalSpliceLocked { + def from(msg: SpliceLocked): ExperimentalSpliceLocked = ExperimentalSpliceLocked(msg.channelId, msg.fundingTxId, msg.tlvStream) +} + case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent @@ -376,6 +420,15 @@ case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, c val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) } +/** This message is used to indicate that the next [[batchSize]] messages form a single logical message. */ +case class StartBatch(channelId: ByteVector32, batchSize: Int, tlvStream: TlvStream[StartBatchTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val messageType_opt: Option[Long] = tlvStream.get[StartBatchTlv.MessageType].map(_.tag) +} + +object StartBatch { + def commitSigBatch(channelId: ByteVector32, batchSize: Int): StartBatch = StartBatch(channelId, batchSize, TlvStream(StartBatchTlv.MessageType(132))) +} + case class UpdateAddHtlc(channelId: ByteVector32, id: Long, amountMsat: MilliSatoshi, @@ -442,7 +495,9 @@ object CommitSigs { case class CommitSig(channelId: ByteVector32, signature: ByteVector64, htlcSignatures: List[ByteVector64], - tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs + tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs { + val fundingTxId_opt: Option[TxId] = tlvStream.get[CommitSigTlv.FundingTx].map(_.txId) +} case class CommitSigBatch(messages: Seq[CommitSig]) extends CommitSigs { require(messages.map(_.channelId).toSet.size == 1, "commit_sig messages in a batch must be for the same channel") 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 404394a62b..63477e0fa8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -111,7 +111,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, Features.ChannelType -> FeatureSupport.Mandatory ), @@ -302,7 +302,7 @@ object TestConstants { Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional, Features.Quiescence -> FeatureSupport.Optional, - Features.SplicePrototype -> FeatureSupport.Optional, + Features.Splicing -> FeatureSupport.Optional, Features.ChannelType -> FeatureSupport.Mandatory ), pluginParams = Nil, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index ee76294cec..58b63a1757 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -204,7 +204,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.SplicePrototype)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.Splicing)) ) (nodeParamsA1, nodeParamsB1) } @@ -703,6 +703,8 @@ object ChannelStateTestsBase { implicit class PimpTestFSM(private val channel: TestFSMRef[ChannelState, ChannelData, Channel]) { val nodeParams: NodeParams = channel.underlyingActor.nodeParams + def commitments: Commitments = channel.stateData.asInstanceOf[ChannelDataWithCommitments].commitments + def setBitcoinCoreFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates) def setBitcoinCoreFeerate(feerate: FeeratePerKw): Unit = setBitcoinCoreFeerates(FeeratesPerKw.single(feerate)) 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 43c8c75aca..623218f2ec 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 @@ -1522,16 +1522,18 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! CMD_SIGN() val sigsA = alice2bob.expectMsgType[CommitSigBatch] assert(sigsA.batchSize == 2) + assert(sigsA.messages.flatMap(_.fundingTxId_opt).toSet == alice.commitments.active.map(_.fundingTxId).toSet) alice2bob.forward(bob, sigsA) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) val sigsB = bob2alice.expectMsgType[CommitSigBatch] assert(sigsB.batchSize == 2) + assert(sigsB.messages.flatMap(_.fundingTxId_opt).toSet == alice.commitments.active.map(_.fundingTxId).toSet) bob2alice.forward(alice, sigsB) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + awaitCond(alice.commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + awaitCond(bob.commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) } test("recv CMD_ADD_HTLC with multiple commitments and reconnect") { f => @@ -1551,11 +1553,13 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) val sigsA = alice2bob.expectMsgType[CommitSigBatch] assert(sigsA.batchSize == 2) + assert(sigsA.messages.flatMap(_.fundingTxId_opt).toSet == alice.commitments.active.map(_.fundingTxId).toSet) alice2bob.forward(bob, sigsA) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) val sigsB = bob2alice.expectMsgType[CommitSigBatch] assert(sigsB.batchSize == 2) + assert(sigsB.messages.flatMap(_.fundingTxId_opt).toSet == alice.commitments.active.map(_.fundingTxId).toSet) bob2alice.forward(alice, sigsB) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) @@ -1682,7 +1686,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob, commitSigsAlice) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - bob2alice.expectMsgType[CommitSig] + assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceTx2.txid)) bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) @@ -1786,10 +1790,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik 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 + val commitSigA = alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + val commitSigB = 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] + assert(commitSigA.fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) + assert(commitSigB.fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) disconnect(f) @@ -1802,7 +1808,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob, channelReestablishAlice1) bob2alice.forward(alice, channelReestablishBob1.copy(nextLocalCommitmentNumber = bobCommitIndex + 1)) // In that case Alice won't retransmit commit_sig and the splice won't complete since they haven't exchanged tx_signatures. - bob2alice.expectMsgType[CommitSig] + assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) bob2alice.forward(alice) alice2bob.expectNoMessage(100 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) @@ -1823,7 +1829,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishBob2.nextLocalCommitmentNumber == bobCommitIndex) // Alice retransmits commit_sig and both retransmit tx_signatures. - alice2bob.expectMsgType[CommitSig] + assert(alice2bob.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) alice2bob.forward(bob) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) @@ -1832,6 +1838,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik sender.expectMsgType[RES_SPLICE] val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + assert(spliceTx.txid == spliceStatus.signingSession.fundingTx.txId) alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) @@ -2476,7 +2483,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } - test("disconnect before channel update and tx_signatures are received") { f=> + test("disconnect before channel update and tx_signatures are received") { f => import f._ // Disconnection with both sides sending tx_signatures and channel updates // alice bob @@ -2515,11 +2522,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val fundingTxIds = alice.commitments.active.map(_.fundingTxId).toSet + assert(fundingTxIds.size == 2) val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) alice ! cmd.copy(commit = true) alice2bob.expectMsgType[UpdateAddHtlc] // Bob doesn't receive Alice's update_add_htlc inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sigs assert(batch.batchSize == 2) + assert(batch.messages.flatMap(_.fundingTxId_opt).toSet == fundingTxIds) } alice2bob.expectNoMessage(100 millis) bob2alice.expectNoMessage(100 millis) @@ -2535,12 +2545,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => assert(batch.batchSize == 2) + assert(batch.messages.flatMap(_.fundingTxId_opt).toSet == fundingTxIds) alice2bob.forward(bob) } bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => assert(batch.batchSize == 2) + assert(batch.messages.flatMap(_.fundingTxId_opt).toSet == fundingTxIds) bob2alice.forward(alice) } alice2bob.expectMsgType[RevokeAndAck] @@ -2593,12 +2605,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() alice ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - alice2bob.expectMsgType[CommitSig] + assert(alice2bob.expectMsgType[CommitSig].fundingTxId_opt.contains(fundingTx.txid)) alice2bob.forward(bob) bob2alice.forward(alice, bobSpliceLocked) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - bob2alice.expectMsgType[CommitSig] + assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(fundingTx.txid)) bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) @@ -2654,7 +2666,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN(None) - inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sig + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sig assert(batch.batchSize == 2) } alice2bob.expectNoMessage(100 millis) @@ -2687,7 +2699,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectNoMessage(100 millis) } - test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => + test("disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => import f._ // alice bob // | ... | @@ -2735,7 +2747,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) assert(channelReestablishAlice.nextRemoteRevocationNumber == 0) - assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextRemoteRevocationNumber == 0) // Bob must retransmit his commit_sigs first. @@ -2752,7 +2764,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectNoMessage(100 millis) } - test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update") { f => + test("disconnection after exchanging tx_signatures and both sides send commit_sig for channel update") { f => import f._ // alice bob // | ... | @@ -2802,12 +2814,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) assert(channelReestablishAlice.nextRemoteRevocationNumber == 1) - assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextRemoteRevocationNumber == 0) // Bob must retransmit his commit_sigs first. alice2bob.expectNoMessage(100 millis) - inside(bob2alice.expectMsgType[CommitSigBatch] ) { batch => + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => assert(batch.batchSize == 2) bob2alice.forward(alice) } 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 24bcd32ebe..1eb6eab12c 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 eb73ade247..1857c95c80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala @@ -126,7 +126,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory test("add liquidity if on-the-fly funding is used", Tag(noPlugin)) { f => import f._ - val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) + val features = defaultFeatures.add(Features.Splicing, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) val open = createOpenDualFundedChannelMessage().copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), @@ -249,7 +249,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory test("reject on-the-fly channel if another channel exists", Tag(noPlugin)) { f => import f._ - val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) + val features = defaultFeatures.add(Features.Splicing, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) val open = createOpenDualFundedChannelMessage().copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index dcefce23aa..1fedc44266 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,11 +19,12 @@ 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._ import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.TestUtils.randomTxId +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} @@ -345,6 +346,8 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi CommitSig(channelId, randomBytes64(), Nil), ) probe.send(peerConnection, CommitSigBatch(commitSigs)) + // We insert a start_batch message. + transport.expectMsg(StartBatch(channelId, batchSize = 3, TlvStream(StartBatchTlv.MessageType(132)))) commitSigs.foreach(commitSig => transport.expectMsg(commitSig)) transport.expectNoMessage(100 millis) } @@ -356,8 +359,8 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive a batch of commit_sig messages from a first channel. val channelId1 = randomBytes32() val commitSigs1 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), ) transport.send(peerConnection, commitSigs1.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs1.head)) @@ -369,9 +372,9 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive a batch of commit_sig messages from a second channel. val channelId2 = randomBytes32() val commitSigs2 = Seq( - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(3))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(3))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(3))), ) commitSigs2.dropRight(1).foreach(commitSig => { transport.send(peerConnection, commitSig) @@ -384,8 +387,8 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive another batch of commit_sig messages from the first channel, with unrelated messages in the batch. val commitSigs3 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), ) transport.send(peerConnection, commitSigs3.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs3.head)) @@ -405,9 +408,9 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We start receiving a batch of commit_sig messages from the first channel, interleaved with a batch from the second // channel, which is not supported. val commitSigs4 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(2))), ) transport.send(peerConnection, commitSigs4.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs4.head)) @@ -420,7 +423,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi peer.expectMsg(CommitSigBatch(commitSigs4.tail)) // We receive a batch that exceeds our threshold: we process them individually. - val invalidCommitSigs = (0 until 30).map(_ => CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(30)))) + val invalidCommitSigs = (0 until 30).map(_ => CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.ExperimentalBatchTlv(30)))) invalidCommitSigs.foreach(commitSig => { transport.send(peerConnection, commitSig) transport.expectMsg(TransportHandler.ReadAck(commitSig)) @@ -428,6 +431,135 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi }) } + test("receive batch of commit_sig messages") { f => + import f._ + connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer) + + // A first channel has a pending splice. + val channelId1 = randomBytes32() + val startBatch1 = StartBatch(channelId1, batchSize = 2, TlvStream(StartBatchTlv.MessageType(132))) + val commitSigs1 = Seq( + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + ) + transport.send(peerConnection, startBatch1) + transport.expectMsg(TransportHandler.ReadAck(startBatch1)) + transport.send(peerConnection, commitSigs1(0)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs1(0))) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs1(1)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs1(1))) + peer.expectMsg(CommitSigBatch(commitSigs1)) + + // Another channel has 2 pending splices. + val channelId2 = randomBytes32() + val startBatch2 = StartBatch(channelId2, batchSize = 3, TlvStream(StartBatchTlv.MessageType(132))) + val commitSigs2 = Seq( + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + ) + transport.send(peerConnection, startBatch2) + transport.expectMsg(TransportHandler.ReadAck(startBatch2)) + transport.send(peerConnection, commitSigs2(0)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs2(0))) + transport.send(peerConnection, commitSigs2(1)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs2(1))) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs2(2)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs2(2))) + peer.expectMsg(CommitSigBatch(commitSigs2)) + + // We receive another batch of commit_sig messages for the first channel. + val startBatch3 = StartBatch(channelId1, batchSize = 3, TlvStream(StartBatchTlv.MessageType(132))) + val commitSigs3 = Seq( + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + ) + transport.send(peerConnection, startBatch3) + transport.expectMsg(TransportHandler.ReadAck(startBatch3)) + transport.send(peerConnection, commitSigs3(0)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs3(0))) + transport.send(peerConnection, commitSigs3(1)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs3(1))) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs3(2)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs3(2))) + peer.expectMsg(CommitSigBatch(commitSigs3)) + + // We reject batches that mix unrelated channels. + val startBatch4 = StartBatch(channelId1, batchSize = 3, TlvStream(StartBatchTlv.MessageType(132))) + val commitSigs4 = Seq( + CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))), + ) + transport.send(peerConnection, startBatch4) + transport.expectMsg(TransportHandler.ReadAck(startBatch4)) + transport.send(peerConnection, commitSigs4(0)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4(0))) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs4(1)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4(1))) + peer.expectMsg(CommitSigBatch(commitSigs4.take(1))) + peer.expectMsg(commitSigs4(1)) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs4(2)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4(2))) + peer.expectMsg(commitSigs4(2)) + peer.expectNoMessage(100 millis) + } + + test("receive unsupported batch of channel messages") { f => + import f._ + connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer) + + // We receive a batch of splice_locked messages: we forward them individually. + val channelId = randomBytes32() + val startBatch1 = StartBatch(channelId, batchSize = 2) // note that start_batch doesn't indicate the message type + val spliceLocked1 = SpliceLocked(channelId, randomTxId()) + val spliceLocked2 = SpliceLocked(channelId, randomTxId()) + transport.send(peerConnection, startBatch1) + transport.expectMsg(TransportHandler.ReadAck(startBatch1)) + transport.expectMsgType[Warning] + transport.send(peerConnection, spliceLocked1) + transport.expectMsg(TransportHandler.ReadAck(spliceLocked1)) + peer.expectMsg(spliceLocked1) + transport.send(peerConnection, spliceLocked2) + transport.expectMsg(TransportHandler.ReadAck(spliceLocked2)) + peer.expectMsg(spliceLocked2) + + // We receive a batch containing commit_sig and an unrelated message. + val startBatch2 = StartBatch(channelId, batchSize = 3, TlvStream(StartBatchTlv.MessageType(132))) + val commitSig1 = CommitSig(channelId, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))) + val commitSig2 = CommitSig(channelId, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))) + val spliceLocked3 = SpliceLocked(channelId, randomTxId()) + transport.send(peerConnection, startBatch2) + transport.expectMsg(TransportHandler.ReadAck(startBatch2)) + transport.send(peerConnection, commitSig1) + transport.expectMsg(TransportHandler.ReadAck(commitSig1)) + transport.send(peerConnection, commitSig2) + transport.expectMsg(TransportHandler.ReadAck(commitSig2)) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, spliceLocked3) + transport.expectMsg(TransportHandler.ReadAck(spliceLocked3)) + peer.expectMsg(CommitSigBatch(commitSig1 :: commitSig2 :: Nil)) + peer.expectMsg(spliceLocked3) + peer.expectNoMessage(100 millis) + + // We receive a batch exceeding 20 elements: we relay messages individually. + val startBatch3 = StartBatch(channelId, batchSize = 21, TlvStream(StartBatchTlv.MessageType(132))) + val commitSig3 = CommitSig(channelId, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(randomTxId()))) + transport.send(peerConnection, startBatch3) + transport.expectMsg(TransportHandler.ReadAck(startBatch3)) + transport.expectMsgType[Warning] + transport.send(peerConnection, commitSig3) + transport.expectMsg(TransportHandler.ReadAck(commitSig3)) + peer.expectMsg(commitSig3) + peer.expectNoMessage(100 millis) + } + test("react to peer's bad behavior") { f => import f._ val probe = TestProbe() @@ -591,5 +723,53 @@ 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 channelId = randomBytes32() + val commitSigBatch = CommitSigBatch(Seq( + CommitSig(channelId, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(TxId(randomBytes32())), CommitSigTlv.ExperimentalBatchTlv(2))), + CommitSig(channelId, randomBytes64(), Nil, TlvStream(CommitSigTlv.FundingTx(TxId(randomBytes32())), CommitSigTlv.ExperimentalBatchTlv(2))), + )) + peer.send(peerConnection, commitSigBatch) + assert(transport.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.FundingTx].isEmpty) + assert(transport.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.FundingTx].isEmpty) + transport.expectNoMessage(100 millis) + } + } 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 aa4731b2f2..9e5fe6e4a2 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 @@ -50,13 +50,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, ) @@ -182,7 +182,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val nodeParams = TestConstants.Alice.nodeParams .modify(_.features.activated).using(_ + (Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional)) .modify(_.features.activated).using(_ + (Features.DualFunding -> FeatureSupport.Optional)) - .modify(_.features.activated).using(_ + (Features.SplicePrototype -> FeatureSupport.Optional)) + .modify(_.features.activated).using(_ + (Features.Splicing -> FeatureSupport.Optional)) .modify(_.features.activated).using(_ + (Features.OnTheFlyFunding -> FeatureSupport.Optional)) .modify(_.features.activated).usingIf(test.tags.contains(withFeeCredit))(_ + (Features.FundingFeeCredit -> FeatureSupport.Optional)) val remoteNodeId = randomKey().publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index ec318af73c..2495982077 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -159,6 +159,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 07 bbbbbbbbbbbbbb" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, hex"bbbbbbbbbbbbbb")))), hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, signature, Nil), + hex"0084" ++ channelId ++ signature ++ hex"0000" ++ hex"01 20" ++ txId.value.reverse -> CommitSig(channelId, signature, Nil, TlvStream(CommitSigTlv.FundingTx(txId))), hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), @@ -205,7 +206,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", TxAddInput(channelId2, UInt64(0), Some(tx2), 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", TxAddInput(channelId1, UInt64(561), Some(tx1), 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", - TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106", + TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 00201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Set.empty[TxAddOutputTlv], Set(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", @@ -214,7 +215,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", - TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025940aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", @@ -390,28 +391,47 @@ 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) + } + } + + test("encode/decode start_batch message") { + val channelId = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val testCases = Seq( + StartBatch(channelId, 1) -> hex"007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0001", + StartBatch(channelId, 7) -> hex"007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0007", + StartBatch.commitSigBatch(channelId, 7) -> hex"007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0007 01020084", + StartBatch(channelId, 7, TlvStream(StartBatchTlv.MessageType(57331))) -> hex"007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0007 0102dff3", + StartBatch(channelId, 32000) -> hex"007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 7d00", + ) + testCases.foreach { case (msg, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded == msg) + val encoded = lightningMessageCodec.encode(msg).require.bytes assert(encoded == bin) } }