From 443266d2b86a1be40e891dd10ad3436c793b5293 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 14 Apr 2022 17:57:38 +0200 Subject: [PATCH] Add dual funding codecs and feature bit (#2231) Add dual funding feature bit, but keep it disabled for now. Add dual funding protocol messages and codecs. We don't actually handle these messages yet. When we receive them, they will simply be ignored and log a warning. --- eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Features.scala | 7 ++ .../eclair/wire/protocol/ChannelTlv.scala | 30 ++++- .../eclair/wire/protocol/CommonCodecs.scala | 12 +- .../wire/protocol/InteractiveTxTlv.scala | 99 +++++++++++++++ .../protocol/LightningMessageCodecs.scala | 117 +++++++++++++++++- .../wire/protocol/LightningMessageTypes.scala | 90 +++++++++++++- .../eclair/wire/protocol/TlvCodecs.scala | 4 + .../scala/fr/acinq/eclair/FeaturesSpec.scala | 73 ++++++----- .../eclair/payment/Bolt11InvoiceSpec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 102 ++++++++++++++- 11 files changed, 499 insertions(+), 38 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 11c5c85181..295ba8a30b 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -54,6 +54,7 @@ eclair { option_anchor_outputs = disabled option_anchors_zero_fee_htlc_tx = optional option_shutdown_anysegwit = optional + option_dual_fund = disabled option_onion_messages = optional option_channel_type = optional option_payment_metadata = optional 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 1e8b2593f9..5eed1bffd6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -213,6 +213,11 @@ object Features { val mandatory = 26 } + case object DualFunding extends Feature with InitFeature with NodeFeature { + val rfcName = "option_dual_fund" + val mandatory = 28 + } + case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 @@ -258,6 +263,7 @@ object Features { AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, + DualFunding, OnionMessages, ChannelType, PaymentMetadata, @@ -272,6 +278,7 @@ object Features { BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), + DualFunding -> (AnchorOutputsZeroFeeHtlcTx :: Nil), TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) 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 7cf67e0671..f49187f7a6 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 @@ -29,17 +29,21 @@ sealed trait OpenChannelTlv extends Tlv sealed trait AcceptChannelTlv extends Tlv +sealed trait OpenDualFundedChannelTlv extends Tlv + +sealed trait AcceptDualFundedChannelTlv extends Tlv + object ChannelTlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ - case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv { + case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv { val isEmpty: Boolean = script.isEmpty } val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScriptTlv] /** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */ - case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv + case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap( b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())), @@ -69,6 +73,28 @@ object AcceptChannelTlv { ) } +object OpenDualFundedChannelTlv { + + import ChannelTlv._ + + val openTlvCodec: Codec[TlvStream[OpenDualFundedChannelTlv]] = tlvStream(discriminated[OpenDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + +object AcceptDualFundedChannelTlv { + + import ChannelTlv._ + + val acceptTlvCodec: Codec[TlvStream[AcceptDualFundedChannelTlv]] = tlvStream(discriminated[AcceptDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index e6c1095fa3..7e37e80478 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Mac32 @@ -100,6 +100,9 @@ object CommonCodecs { // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63. val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) + // This codec can be safely used for values < 2^32 and will fail otherwise. + val smallvarint: Codec[Int] = varint.narrow(l => if (l <= UInt64(Int.MaxValue)) Attempt.successful(l.toBigInt.toInt) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) + val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes)) val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes)) @@ -112,6 +115,11 @@ object CommonCodecs { val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] + val extendedChannelFlags: Codec[ChannelFlags] = variableSizeBytesLong(varintoverflow, bytes).xmap( + bin => ChannelFlags(bin.lastOption.exists(_ % 2 == 1)), + flags => if (flags.announceChannel) ByteVector(1) else ByteVector(0) + ) + val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) @@ -144,6 +152,8 @@ object CommonCodecs { val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) + val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) + def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(size, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) /** 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 new file mode 100644 index 0000000000..5776eaa5ac --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.scalacompat.Satoshi +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.protocol.CommonCodecs.{varint, varintoverflow} +import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvStream, tsatoshi} +import scodec.Codec +import scodec.codecs.{discriminated, variableSizeBytesLong} + +/** + * Created by t-bast on 08/04/2022. + */ + +sealed trait TxAddInputTlv extends Tlv + +object TxAddInputTlv { + val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)) +} + +sealed trait TxAddOutputTlv extends Tlv + +object TxAddOutputTlv { + val txAddOutputTlvCodec: Codec[TlvStream[TxAddOutputTlv]] = tlvStream(discriminated[TxAddOutputTlv].by(varint)) +} + +sealed trait TxRemoveInputTlv extends Tlv + +object TxRemoveInputTlv { + val txRemoveInputTlvCodec: Codec[TlvStream[TxRemoveInputTlv]] = tlvStream(discriminated[TxRemoveInputTlv].by(varint)) +} + +sealed trait TxRemoveOutputTlv extends Tlv + +object TxRemoveOutputTlv { + val txRemoveOutputTlvCodec: Codec[TlvStream[TxRemoveOutputTlv]] = tlvStream(discriminated[TxRemoveOutputTlv].by(varint)) +} + +sealed trait TxCompleteTlv extends Tlv + +object TxCompleteTlv { + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) +} + +sealed trait TxSignaturesTlv extends Tlv + +object TxSignaturesTlv { + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)) +} + +sealed trait TxInitRbfTlv extends Tlv + +sealed trait TxAckRbfTlv extends Tlv + +object TxRbfTlv { + /** Amount that the peer will contribute to the transaction's shared output. */ + case class SharedOutputContributionTlv(amount: Satoshi) extends TxInitRbfTlv with TxAckRbfTlv +} + +object TxInitRbfTlv { + + import TxRbfTlv._ + + val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +object TxAckRbfTlv { + + import TxRbfTlv._ + + val txAckRbfTlvCodec: Codec[TlvStream[TxAckRbfTlv]] = tlvStream(discriminated[TxAckRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +sealed trait TxAbortTlv extends Tlv + +object TxAbortTlv { + val txAbortTlvCodec: Codec[TlvStream[TxAbortTlv]] = tlvStream(discriminated[TxAbortTlv].by(varint)) +} \ No newline at end of file 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 774adac8d0..620c91c3ab 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 @@ -16,9 +16,10 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.ScriptWitness import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt, NodeFeature} +import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -81,7 +82,7 @@ object LightningMessageCodecs { ("fundingSatoshis" | satoshi) :: ("pushMsat" | millisatoshi) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("feeratePerKw" | feeratePerKw) :: @@ -96,10 +97,31 @@ object LightningMessageCodecs { ("channelFlags" | channelflags) :: ("tlvStream" | OpenChannelTlv.openTlvCodec)).as[OpenChannel] + val openDualFundedChannelCodec: Codec[OpenDualFundedChannel] = ( + ("chainHash" | bytes32) :: + ("temporaryChannelId" | bytes32) :: + ("fundingFeerate" | feeratePerKw) :: + ("commitmentFeerate" | feeratePerKw) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("lockTime" | uint32) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("channelFlags" | extendedChannelFlags) :: + ("tlvStream" | OpenDualFundedChannelTlv.openTlvCodec)).as[OpenDualFundedChannel] + val acceptChannelCodec: Codec[AcceptChannel] = ( ("temporaryChannelId" | bytes32) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("minimumDepth" | uint32) :: @@ -113,6 +135,24 @@ object LightningMessageCodecs { ("firstPerCommitmentPoint" | publicKey) :: ("tlvStream" | AcceptChannelTlv.acceptTlvCodec)).as[AcceptChannel] + val acceptDualFundedChannelCodec: Codec[AcceptDualFundedChannel] = ( + ("temporaryChannelId" | bytes32) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("minimumDepth" | uint32) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("channelFlags" | extendedChannelFlags) :: + ("tlvStream" | AcceptDualFundedChannelTlv.acceptTlvCodec)).as[AcceptDualFundedChannel] + val fundingCreatedCodec: Codec[FundingCreated] = ( ("temporaryChannelId" | bytes32) :: ("fundingTxid" | bytes32) :: @@ -130,6 +170,66 @@ object LightningMessageCodecs { ("nextPerCommitmentPoint" | publicKey) :: ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] + private val scriptSigOptCodec: Codec[Option[ByteVector]] = lengthDelimited(bytes).xmap[Option[ByteVector]]( + b => if (b.isEmpty) None else Some(b), + b => b.getOrElse(ByteVector.empty) + ) + + val txAddInputCodec: Codec[TxAddInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("previousTx" | lengthDelimited(txCodec)) :: + ("previousTxOutput" | uint32) :: + ("sequence" | uint32) :: + ("scriptSig" | scriptSigOptCodec) :: + ("tlvStream" | TxAddInputTlv.txAddInputTlvCodec)).as[TxAddInput] + + val txAddOutputCodec: Codec[TxAddOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes)) :: + ("tlvStream" | TxAddOutputTlv.txAddOutputTlvCodec)).as[TxAddOutput] + + val txRemoveInputCodec: Codec[TxRemoveInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveInputTlv.txRemoveInputTlvCodec)).as[TxRemoveInput] + + val txRemoveOutputCodec: Codec[TxRemoveOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveOutputTlv.txRemoveOutputTlvCodec)).as[TxRemoveOutput] + + val txCompleteCodec: Codec[TxComplete] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxCompleteTlv.txCompleteTlvCodec)).as[TxComplete] + + private val witnessElementCodec: Codec[ByteVector] = lengthDelimited(bytes) + private val witnessStackCodec: Codec[ScriptWitness] = listOfN(smallvarint, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(smallvarint, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) + + val txSignaturesCodec: Codec[TxSignatures] = ( + ("channelId" | bytes32) :: + ("txId" | sha256) :: + ("witnesses" | witnessesCodec) :: + ("tlvStream" | TxSignaturesTlv.txSignaturesTlvCodec)).as[TxSignatures] + + val txInitRbfCodec: Codec[TxInitRbf] = ( + ("channelId" | bytes32) :: + ("lockTime" | uint32) :: + ("feerate" | feeratePerKw) :: + ("tlvStream" | TxInitRbfTlv.txInitRbfTlvCodec)).as[TxInitRbf] + + val txAckRbfCodec: Codec[TxAckRbf] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxAckRbfTlv.txAckRbfTlvCodec)).as[TxAckRbf] + + val txAbortCodec: Codec[TxAbort] = ( + ("channelId" | bytes32) :: + ("data" | lengthDelimited(bytes)) :: + ("tlvStream" | TxAbortTlv.txAbortTlvCodec)).as[TxAbort] + val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: ("scriptPubKey" | varsizebinarydata) :: @@ -351,6 +451,17 @@ object LightningMessageCodecs { .typecase(36, fundingLockedCodec) .typecase(38, shutdownCodec) .typecase(39, closingSignedCodec) + .typecase(64, openDualFundedChannelCodec) + .typecase(65, acceptDualFundedChannelCodec) + .typecase(66, txAddInputCodec) + .typecase(67, txAddOutputCodec) + .typecase(68, txRemoveInputCodec) + .typecase(69, txRemoveOutputCodec) + .typecase(70, txCompleteCodec) + .typecase(71, txSignaturesCodec) + .typecase(72, txInitRbfCodec) + .typecase(73, txAckRbfCodec) + .typecase(74, txAbortCodec) .typecase(128, updateAddHtlcCodec) .typecase(130, updateFulfillHtlcCodec) .typecase(131, updateFailHtlcCodec) 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 9880e02781..6510657c48 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} @@ -37,6 +37,7 @@ import scala.util.Try sealed trait LightningMessage extends Serializable sealed trait SetupMessage extends LightningMessage sealed trait ChannelMessage extends LightningMessage +sealed trait InteractiveTxMessage extends LightningMessage sealed trait HtlcMessage extends LightningMessage sealed trait RoutingMessage extends LightningMessage sealed trait AnnouncementMessage extends RoutingMessage // <- not in the spec @@ -79,6 +80,48 @@ case class Ping(pongLength: Int, data: ByteVector, tlvStream: TlvStream[PingTlv] case class Pong(data: ByteVector, tlvStream: TlvStream[PongTlv] = TlvStream.empty) extends SetupMessage +case class TxAddInput(channelId: ByteVector32, + serialId: UInt64, + previousTx: Transaction, + previousTxOutput: Long, + sequence: Long, + scriptSig_opt: Option[ByteVector], + tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAddOutput(channelId: ByteVector32, + serialId: UInt64, + amount: Satoshi, + pubkeyScript: ByteVector, + tlvStream: TlvStream[TxAddOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveInput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveOutput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxComplete(channelId: ByteVector32, + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxSignatures(channelId: ByteVector32, + txId: ByteVector32, + witnesses: Seq[ScriptWitness], + tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxInitRbf(channelId: ByteVector32, + lockTime: Long, + feerate: FeeratePerKw, + tlvStream: TlvStream[TxInitRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAckRbf(channelId: ByteVector32, + tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAbort(channelId: ByteVector32, + data: ByteVector, + tlvStream: TlvStream[TxAbortTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + case class ChannelReestablish(channelId: ByteVector32, nextLocalCommitmentNumber: Long, nextRemoteRevocationNumber: Long, @@ -128,6 +171,51 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) } +// NB: this message is named open_channel2 in the specification. +case class OpenDualFundedChannel(chainHash: ByteVector32, + temporaryChannelId: ByteVector32, + fundingFeerate: FeeratePerKw, + commitmentFeerate: FeeratePerKw, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + lockTime: Long, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + tlvStream: TlvStream[OpenDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + +// NB: this message is named accept_channel2 in the specification. +case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + minimumDepth: Long, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + tlvStream: TlvStream[AcceptDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxid: ByteVector32, fundingOutputIndex: Int, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index 932ad7f1e0..42798b4ad0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{minimalvalue, uint64, varint, varintoverflow} import fr.acinq.eclair.{MilliSatoshi, UInt64} @@ -77,6 +78,9 @@ object TlvCodecs { */ val tmillisatoshi: Codec[MilliSatoshi] = tu64overflow.xmap(l => MilliSatoshi(l), m => m.toLong) + /** Truncated satoshi (0 to 8 bytes unsigned). */ + val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong) + /** Truncated uint32 (0 to 4 bytes unsigned integer). */ val tu32: Codec[Long] = tu64.exmap({ case i if i > 0xffffffffL => Attempt.Failure(Err("tu32 overflow")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index e3ef702c97..65c83af986 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -62,39 +62,54 @@ class FeaturesSpec extends AnyFunSuite { test("features dependencies") { val testCases = Map( - bin" " -> true, - bin" 00000000" -> true, - bin" 01011000" -> true, + bin" " -> true, + bin" 00000000" -> true, + bin" 01011000" -> true, // gossip_queries_ex depend on gossip_queries - bin"000000000000100000000000" -> false, - bin"000000000000010000000000" -> false, - bin"000000000000100010000000" -> true, - bin"000000000000100001000000" -> true, + bin" 000000000000100000000000" -> false, + bin" 000000000000010000000000" -> false, + bin" 000000000000100010000000" -> true, + bin" 000000000000100001000000" -> true, // payment_secret depends on var_onion_optin - bin"000000001000000000000000" -> false, - bin"000000000100000000000000" -> false, - bin"000000000100001000000000" -> true, + bin" 000000001000000000000000" -> false, + bin" 000000000100000000000000" -> false, + bin" 000000000100001000000000" -> true, // basic_mpp depends on payment_secret - bin"000000100000000000000000" -> false, - bin"000000010000000000000000" -> false, - bin"000000101000000100000000" -> true, - bin"000000011000000100000000" -> true, - bin"000000011000001000000000" -> true, - bin"000000100100000100000000" -> true, + bin" 000000100000000000000000" -> false, + bin" 000000010000000000000000" -> false, + bin" 000000101000000100000000" -> true, + bin" 000000011000000100000000" -> true, + bin" 000000011000001000000000" -> true, + bin" 000000100100000100000000" -> true, // option_anchor_outputs depends on option_static_remotekey - bin"001000000000000000000000" -> false, - bin"000100000000000000000000" -> false, - bin"001000000010000000000000" -> true, - bin"001000000001000000000000" -> true, - bin"000100000010000000000000" -> true, - bin"000100000001000000000000" -> true, + bin" 001000000000000000000000" -> false, + bin" 000100000000000000000000" -> false, + bin" 001000000010000000000000" -> true, + bin" 001000000001000000000000" -> true, + bin" 000100000010000000000000" -> true, + bin" 000100000001000000000000" -> true, // option_anchors_zero_fee_htlc_tx depends on option_static_remotekey - bin"100000000000000000000000" -> false, - bin"010000000000000000000000" -> false, - bin"100000000010000000000000" -> true, - bin"100000000001000000000000" -> true, - bin"010000000010000000000000" -> true, - bin"010000000001000000000000" -> true, + bin" 100000000000000000000000" -> false, + bin" 010000000000000000000000" -> false, + bin" 100000000010000000000000" -> true, + bin" 100000000001000000000000" -> true, + bin" 010000000010000000000000" -> true, + bin" 010000000001000000000000" -> true, + // option_dual_fund depends on option_anchors_zero_fee_htlc_tx, which itself depends on option_static_remotekey + bin"00100000000000000000000000000000" -> false, + bin"00010000000000000000000000000000" -> false, + bin"00100000100000000000000000000000" -> false, + bin"00100000010000000000000000000000" -> false, + bin"00010000100000000000000000000000" -> false, + bin"00010000010000000000000000000000" -> false, + bin"00100000100000000010000000000000" -> true, + bin"00100000100000000001000000000000" -> true, + bin"00100000010000000010000000000000" -> true, + bin"00100000010000000001000000000000" -> true, + bin"00010000100000000010000000000000" -> true, + bin"00010000100000000001000000000000" -> true, + bin"00010000010000000010000000000000" -> true, + bin"00010000010000000001000000000000" -> true, ) for ((testCase, valid) <- testCases) { @@ -236,7 +251,7 @@ class FeaturesSpec extends AnyFunSuite { hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), - hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + hex"80010080000000000000000000000000000000000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(151), UnknownFeature(160), UnknownFeature(175))) ) for ((bin, features) <- testCases) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index bcb9adda8b..d68870976a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -480,7 +480,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) Features(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), Features(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), + Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), Features(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) ) 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 389141e7b8..317f744309 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 @@ -16,8 +16,9 @@ package fr.acinq.eclair.wire.protocol +import com.google.common.base.Charsets import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong, ScriptWitness, Transaction} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair._ @@ -25,8 +26,10 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.protocol.ChannelTlv.{ChannelTypeTlv, UpfrontShutdownScriptTlv} import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ +import fr.acinq.eclair.wire.protocol.TxRbfTlv.SharedOutputContributionTlv import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult @@ -153,6 +156,43 @@ class LightningMessageCodecsSpec extends AnyFunSuite { assert(bin === bin2) } + test("encode/decode interactive-tx messages") { + val channelId1 = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val channelId2 = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + // This is a random mainnet transaction. + val txBin1 = hex"020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" + val tx1 = Transaction.read(txBin1.toArray) + // This is random, longer mainnet transaction. + val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" + val tx2 = Transaction.read(txBin2.toArray) + val testCases = Seq( + TxAddInput(channelId1, UInt64(561), tx1, 1, 5, None) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 00", + TxAddInput(channelId2, UInt64(0), tx2, 2, 0, None) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 fd0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000 00", + TxAddInput(channelId1, UInt64(561), tx1, 0, 0, Some(hex"00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000 15 00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", + TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", + TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", + TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + TxComplete(channelId1, TlvStream(Nil, Seq(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", + TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 02 0202dead02beef 04 000401010101000102", + TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 00", + TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", + TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream(SharedOutputContributionTlv(5000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00021388", + TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxAckRbf(channelId2, TlvStream(SharedOutputContributionTlv(450000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000306ddd0", + TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00", + TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0e 696e7465726e616c206572726f72", + ) + testCases.foreach { case (message, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require + assert(decoded.remainder.isEmpty) + assert(decoded.value === message) + val encoded = lightningMessageCodec.encode(message).require.bytes + assert(encoded === bin) + } + } + test("encode/decode open_channel") { val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. @@ -210,6 +250,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode open_channel (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val testCases = Seq( + defaultOpen -> defaultEncoded, + defaultOpen.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), + ) + testCases.foreach { case (open, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === open) + val encoded = lightningMessageCodec.encode(open).require.bytes + assert(encoded === bin) + } + } + + test("decode open_channel with unknown channel flags (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncodedWithoutFlags = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + ) + testCases.foreach { case (bin, flags) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === defaultOpen.copy(channelFlags = flags)) + } + } + test("encode/decode accept_channel") { val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6)) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. @@ -236,6 +306,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode accept_channel (dual funding)") { + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) + val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val testCases = Seq( + defaultAccept -> defaultEncoded, + defaultAccept.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283"), + defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey))) -> (defaultEncoded ++ hex"01021000"), + ) + testCases.foreach { case (accept, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === accept) + val encoded = lightningMessageCodec.encode(accept).require.bytes + assert(encoded === bin) + } + } + + test("decode accept_channel with unknown channel flags (dual funding)") { + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) + val defaultEncodedWithoutFlags = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + ) + testCases.foreach { case (bin, flags) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === defaultAccept.copy(channelFlags = flags)) + } + } + test("encode/decode closing_signed") { val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val testCases = Seq(