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(