diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index 948c166bd..7a288a471 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -95,6 +95,11 @@ # Technical and Architectural Updates ## BOLT Spec Updates + +* [Add Dynamic Commitment Wire Types](https://github.com/lightningnetwork/lnd/pull/8026). + This change begins the development of Dynamic Commitments allowing for the + negotiation of new channel parameters and the upgrading of channel types. + ## Testing * Added fuzz tests for [onion diff --git a/fn/option.go b/fn/option.go new file mode 100644 index 000000000..a2c3afdc2 --- /dev/null +++ b/fn/option.go @@ -0,0 +1,149 @@ +package fn + +// Option[A] represents a value which may or may not be there. This is very +// often preferable to nil-able pointers. +type Option[A any] struct { + isSome bool + some A +} + +// Some trivially injects a value into an optional context. +// +// Some : A -> Option[A]. +func Some[A any](a A) Option[A] { + return Option[A]{ + isSome: true, + some: a, + } +} + +// None trivially constructs an empty option +// +// None : Option[A]. +func None[A any]() Option[A] { + return Option[A]{} +} + +// ElimOption is the universal Option eliminator. It can be used to safely +// handle all possible values inside the Option by supplying two continuations. +// +// ElimOption : (Option[A], () -> B, A -> B) -> B. +func ElimOption[A, B any](o Option[A], b func() B, f func(A) B) B { + if o.isSome { + return f(o.some) + } + + return b() +} + +// UnwrapOr is used to extract a value from an option, and we supply the default +// value in the case when the Option is empty. +// +// UnwrapOr : (Option[A], A) -> A. +func (o Option[A]) UnwrapOr(a A) A { + if o.isSome { + return o.some + } + + return a +} + +// WhenSome is used to conditionally perform a side-effecting function that +// accepts a value of the type that parameterizes the option. If this function +// performs no side effects, WhenSome is useless. +// +// WhenSome : (Option[A], A -> ()) -> (). +func (o Option[A]) WhenSome(f func(A)) { + if o.isSome { + f(o.some) + } +} + +// IsSome returns true if the Option contains a value +// +// IsSome : Option[A] -> bool. +func (o Option[A]) IsSome() bool { + return o.isSome +} + +// IsNone returns true if the Option is empty +// +// IsNone : Option[A] -> bool. +func (o Option[A]) IsNone() bool { + return !o.isSome +} + +// FlattenOption joins multiple layers of Options together such that if any of +// the layers is None, then the joined value is None. Otherwise the innermost +// Some value is returned. +// +// FlattenOption : Option[Option[A]] -> Option[A]. +func FlattenOption[A any](oo Option[Option[A]]) Option[A] { + if oo.IsNone() { + return None[A]() + } + if oo.some.IsNone() { + return None[A]() + } + + return oo.some +} + +// ChainOption transforms a function A -> Option[B] into one that accepts an +// Option[A] as an argument. +// +// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B]. +func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] { + return func(o Option[A]) Option[B] { + if o.isSome { + return f(o.some) + } + + return None[B]() + } +} + +// MapOption transforms a pure function A -> B into one that will operate +// inside the Option context. +// +// MapOption : (A -> B) -> Option[A] -> Option[B]. +func MapOption[A, B any](f func(A) B) func(Option[A]) Option[B] { + return func(o Option[A]) Option[B] { + if o.isSome { + return Some(f(o.some)) + } + + return None[B]() + } +} + +// LiftA2Option transforms a pure function (A, B) -> C into one that will +// operate in an Option context. For the returned function, if either of its +// arguments are None, then the result will be None. +// +// LiftA2Option : ((A, B) -> C) -> (Option[A], Option[B]) -> Option[C]. +func LiftA2Option[A, B, C any]( + f func(A, B) C, +) func(Option[A], Option[B]) Option[C] { + + return func(o1 Option[A], o2 Option[B]) Option[C] { + if o1.isSome && o2.isSome { + return Some(f(o1.some, o2.some)) + } + + return None[C]() + } +} + +// Alt chooses the left Option if it is full, otherwise it chooses the right +// option. This can be useful in a long chain if you want to choose between +// many different ways of producing the needed value. +// +// Alt : Option[A] -> Option[A] -> Option[A]. +func (o Option[A]) Alt(o2 Option[A]) Option[A] { + if o.isSome { + return o + } + + return o2 +} diff --git a/lnwire/channel_reestablish.go b/lnwire/channel_reestablish.go index 1b6cfdffc..b4a5258c8 100644 --- a/lnwire/channel_reestablish.go +++ b/lnwire/channel_reestablish.go @@ -5,9 +5,24 @@ import ( "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/tlv" ) +const ( + CRDynHeight tlv.Type = 20 +) + +// DynHeight is a newtype wrapper to get the proper RecordProducer instance +// to smoothly integrate with the ChannelReestablish Message instance. +type DynHeight uint64 + +// Record implements the RecordProducer interface, allowing a full tlv.Record +// object to be constructed from a DynHeight. +func (d *DynHeight) Record() tlv.Record { + return tlv.MakePrimitiveRecord(CRDynHeight, (*uint64)(d)) +} + // ChannelReestablish is a message sent between peers that have an existing // open channel upon connection reestablishment. This message allows both sides // to report their local state, and their current knowledge of the state of the @@ -70,6 +85,11 @@ type ChannelReestablish struct { // TODO(roasbeef): rename to verification nonce LocalNonce *Musig2Nonce + // DynHeight is an optional field that stores the dynamic commitment + // negotiation height that is incremented upon successful completion of + // a dynamic commitment negotiation + DynHeight fn.Option[DynHeight] + // ExtraData is the set of data that was appended to this message to // fill out the full maximum transport message size. These fields can // be used to specify optional data such as custom TLV fields. @@ -121,6 +141,10 @@ func (a *ChannelReestablish) Encode(w *bytes.Buffer, pver uint32) error { if a.LocalNonce != nil { recordProducers = append(recordProducers, a.LocalNonce) } + a.DynHeight.WhenSome(func(h DynHeight) { + recordProducers = append(recordProducers, &h) + }) + err := EncodeMessageExtraData(&a.ExtraData, recordProducers...) if err != nil { return err @@ -180,8 +204,9 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error { } var localNonce Musig2Nonce + var dynHeight DynHeight typeMap, err := tlvRecords.ExtractRecords( - &localNonce, + &localNonce, &dynHeight, ) if err != nil { return err @@ -190,6 +215,9 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error { if val, ok := typeMap[NonceRecordType]; ok && val == nil { a.LocalNonce = &localNonce } + if val, ok := typeMap[CRDynHeight]; ok && val == nil { + a.DynHeight = fn.Some(dynHeight) + } if len(tlvRecords) != 0 { a.ExtraData = tlvRecords diff --git a/lnwire/dyn_ack.go b/lnwire/dyn_ack.go new file mode 100644 index 000000000..24f23a228 --- /dev/null +++ b/lnwire/dyn_ack.go @@ -0,0 +1,138 @@ +package lnwire + +import ( + "bytes" + "io" + + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // DALocalMusig2Pubnonce is the TLV type number that identifies the + // musig2 public nonce that we need to verify the commitment transaction + // signature. + DALocalMusig2Pubnonce tlv.Type = 0 +) + +// DynAck is the message used to accept the parameters of a dynamic commitment +// negotiation. Additional optional parameters will need to be present depending +// on the details of the dynamic commitment upgrade. +type DynAck struct { + // ChanID is the ChannelID of the channel that is currently undergoing + // a dynamic commitment negotiation + ChanID ChannelID + + // LocalNonce is an optional field that is transmitted when accepting + // a dynamic commitment upgrade to Taproot Channels. This nonce will be + // used to verify the first commitment transaction signature. This will + // only be populated if the DynPropose we are responding to specifies + // taproot channels in the ChannelType field. + LocalNonce fn.Option[Musig2Nonce] + + // ExtraData is the set of data that was appended to this message to + // fill out the full maximum transport message size. These fields can + // be used to specify optional data such as custom TLV fields. + ExtraData ExtraOpaqueData +} + +// A compile time check to ensure DynAck implements the lnwire.Message +// interface. +var _ Message = (*DynAck)(nil) + +// Encode serializes the target DynAck into the passed io.Writer. Serialization +// will observe the rules defined by the passed protocol version. +// +// This is a part of the lnwire.Message interface. +func (da *DynAck) Encode(w *bytes.Buffer, _ uint32) error { + if err := WriteChannelID(w, da.ChanID); err != nil { + return err + } + + var tlvRecords []tlv.Record + da.LocalNonce.WhenSome(func(nonce Musig2Nonce) { + tlvRecords = append( + tlvRecords, tlv.MakeStaticRecord( + DALocalMusig2Pubnonce, &nonce, + musig2.PubNonceSize, nonceTypeEncoder, + nonceTypeDecoder, + ), + ) + }) + tlv.SortRecords(tlvRecords) + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + var extraBytesWriter bytes.Buffer + if err := tlvStream.Encode(&extraBytesWriter); err != nil { + return err + } + + da.ExtraData = ExtraOpaqueData(extraBytesWriter.Bytes()) + + return WriteBytes(w, da.ExtraData) +} + +// Decode deserializes the serialized DynAck stored in the passed io.Reader into +// the target DynAck using the deserialization rules defined by the passed +// protocol version. +// +// This is a part of the lnwire.Message interface. +func (da *DynAck) Decode(r io.Reader, _ uint32) error { + // Parse out main message. + if err := ReadElements(r, &da.ChanID); err != nil { + return err + } + + // Parse out TLV records. + var tlvRecords ExtraOpaqueData + if err := ReadElement(r, &tlvRecords); err != nil { + return err + } + + // Prepare receiving buffers to be filled by TLV extraction. + var localNonceScratch Musig2Nonce + localNonce := tlv.MakeStaticRecord( + DALocalMusig2Pubnonce, &localNonceScratch, musig2.PubNonceSize, + nonceTypeEncoder, nonceTypeDecoder, + ) + + // Create set of Records to read TLV bytestream into. + records := []tlv.Record{localNonce} + tlv.SortRecords(records) + + // Read TLV stream into record set. + extraBytesReader := bytes.NewReader(tlvRecords) + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + typeMap, err := tlvStream.DecodeWithParsedTypesP2P(extraBytesReader) + if err != nil { + return err + } + + // Check the results of the TLV Stream decoding and appropriately set + // message fields. + if val, ok := typeMap[DALocalMusig2Pubnonce]; ok && val == nil { + da.LocalNonce = fn.Some(localNonceScratch) + } + + if len(tlvRecords) != 0 { + da.ExtraData = tlvRecords + } + + return nil +} + +// MsgType returns the MessageType code which uniquely identifies this message +// as a DynAck on the wire. +// +// This is part of the lnwire.Message interface. +func (da *DynAck) MsgType() MessageType { + return MsgDynAck +} diff --git a/lnwire/dyn_propose.go b/lnwire/dyn_propose.go new file mode 100644 index 000000000..b0cc1198e --- /dev/null +++ b/lnwire/dyn_propose.go @@ -0,0 +1,319 @@ +package lnwire + +import ( + "bytes" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // DPDustLimitSatoshis is the TLV type number that identifies the record + // for DynPropose.DustLimit. + DPDustLimitSatoshis tlv.Type = 0 + + // DPMaxHtlcValueInFlightMsat is the TLV type number that identifies the + // record for DynPropose.MaxValueInFlight. + DPMaxHtlcValueInFlightMsat tlv.Type = 1 + + // DPChannelReserveSatoshis is the TLV type number that identifies the + // for DynPropose.ChannelReserve. + DPChannelReserveSatoshis tlv.Type = 2 + + // DPToSelfDelay is the TLV type number that identifies the record for + // DynPropose.CsvDelay. + DPToSelfDelay tlv.Type = 3 + + // DPMaxAcceptedHtlcs is the TLV type number that identifies the record + // for DynPropose.MaxAcceptedHTLCs. + DPMaxAcceptedHtlcs tlv.Type = 4 + + // DPFundingPubkey is the TLV type number that identifies the record for + // DynPropose.FundingKey. + DPFundingPubkey tlv.Type = 5 + + // DPChannelType is the TLV type number that identifies the record for + // DynPropose.ChannelType. + DPChannelType tlv.Type = 6 + + // DPKickoffFeerate is the TLV type number that identifies the record + // for DynPropose.KickoffFeerate. + DPKickoffFeerate tlv.Type = 7 +) + +// DynPropose is a message that is sent during a dynamic commitments negotiation +// process. It is sent by both parties to propose new channel parameters. +type DynPropose struct { + // ChanID identifies the channel whose parameters we are trying to + // re-negotiate. + ChanID ChannelID + + // Initiator is a byte that identifies whether this message was sent as + // the initiator of a dynamic commitment negotiation or the responder + // of a dynamic commitment negotiation. bool true indicates it is the + // initiator + Initiator bool + + // DustLimit, if not nil, proposes a change to the dust_limit_satoshis + // for the sender's commitment transaction. + DustLimit fn.Option[btcutil.Amount] + + // MaxValueInFlight, if not nil, proposes a change to the + // max_htlc_value_in_flight_msat limit of the sender. + MaxValueInFlight fn.Option[MilliSatoshi] + + // ChannelReserve, if not nil, proposes a change to the + // channel_reserve_satoshis requirement of the recipient. + ChannelReserve fn.Option[btcutil.Amount] + + // CsvDelay, if not nil, proposes a change to the to_self_delay + // requirement of the recipient. + CsvDelay fn.Option[uint16] + + // MaxAcceptedHTLCs, if not nil, proposes a change to the + // max_accepted_htlcs limit of the sender. + MaxAcceptedHTLCs fn.Option[uint16] + + // FundingKey, if not nil, proposes a change to the funding_pubkey + // parameter of the sender. + FundingKey fn.Option[btcec.PublicKey] + + // ChannelType, if not nil, proposes a change to the channel_type + // parameter. + ChannelType fn.Option[ChannelType] + + // KickoffFeerate proposes the fee rate in satoshis per kw that it + // is offering for a ChannelType conversion that requires a kickoff + // transaction. + KickoffFeerate fn.Option[chainfee.SatPerKWeight] + + // ExtraData is the set of data that was appended to this message to + // fill out the full maximum transport message size. These fields can + // be used to specify optional data such as custom TLV fields. + // + // NOTE: Since the fields in this structure are part of the TLV stream, + // ExtraData will contain all TLV records _except_ the ones that are + // present in earlier parts of this structure. + ExtraData ExtraOpaqueData +} + +// A compile time check to ensure DynPropose implements the lnwire.Message +// interface. +var _ Message = (*DynPropose)(nil) + +// Encode serializes the target DynPropose into the passed io.Writer. +// Serialization will observe the rules defined by the passed protocol version. +// +// This is a part of the lnwire.Message interface. +func (dp *DynPropose) Encode(w *bytes.Buffer, _ uint32) error { + var tlvRecords []tlv.Record + dp.DustLimit.WhenSome(func(dl btcutil.Amount) { + protoSats := uint64(dl) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPDustLimitSatoshis, &protoSats, + ), + ) + }) + dp.MaxValueInFlight.WhenSome(func(max MilliSatoshi) { + protoSats := uint64(max) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPMaxHtlcValueInFlightMsat, &protoSats, + ), + ) + }) + dp.ChannelReserve.WhenSome(func(min btcutil.Amount) { + channelReserve := uint64(min) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPChannelReserveSatoshis, &channelReserve, + ), + ) + }) + dp.CsvDelay.WhenSome(func(wait uint16) { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPToSelfDelay, &wait, + ), + ) + }) + dp.MaxAcceptedHTLCs.WhenSome(func(max uint16) { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPMaxAcceptedHtlcs, &max, + ), + ) + }) + dp.FundingKey.WhenSome(func(key btcec.PublicKey) { + keyScratch := &key + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPFundingPubkey, &keyScratch, + ), + ) + }) + dp.ChannelType.WhenSome(func(ty ChannelType) { + tlvRecords = append( + tlvRecords, tlv.MakeDynamicRecord( + DPChannelType, &ty, + ty.featureBitLen, + channelTypeEncoder, channelTypeDecoder, + ), + ) + }) + dp.KickoffFeerate.WhenSome(func(kickoffFeerate chainfee.SatPerKWeight) { + protoSats := uint32(kickoffFeerate) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPKickoffFeerate, &protoSats, + ), + ) + }) + tlv.SortRecords(tlvRecords) + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + var extraBytesWriter bytes.Buffer + if err := tlvStream.Encode(&extraBytesWriter); err != nil { + return err + } + dp.ExtraData = ExtraOpaqueData(extraBytesWriter.Bytes()) + + if err := WriteChannelID(w, dp.ChanID); err != nil { + return err + } + + if err := WriteBool(w, dp.Initiator); err != nil { + return err + } + + return WriteBytes(w, dp.ExtraData) +} + +// Decode deserializes the serialized DynPropose stored in the passed io.Reader +// into the target DynPropose using the deserialization rules defined by the +// passed protocol version. +// +// This is a part of the lnwire.Message interface. +func (dp *DynPropose) Decode(r io.Reader, _ uint32) error { + // Parse out the only required field. + if err := ReadElements(r, &dp.ChanID, &dp.Initiator); err != nil { + return err + } + + // Parse out TLV stream. + var tlvRecords ExtraOpaqueData + if err := ReadElements(r, &tlvRecords); err != nil { + return err + } + + // Prepare receiving buffers to be filled by TLV extraction. + var dustLimitScratch uint64 + dustLimit := tlv.MakePrimitiveRecord( + DPDustLimitSatoshis, &dustLimitScratch, + ) + + var maxValueScratch uint64 + maxValue := tlv.MakePrimitiveRecord( + DPMaxHtlcValueInFlightMsat, &maxValueScratch, + ) + + var reserveScratch uint64 + reserve := tlv.MakePrimitiveRecord( + DPChannelReserveSatoshis, &reserveScratch, + ) + + var csvDelayScratch uint16 + csvDelay := tlv.MakePrimitiveRecord(DPToSelfDelay, &csvDelayScratch) + + var maxHtlcsScratch uint16 + maxHtlcs := tlv.MakePrimitiveRecord( + DPMaxAcceptedHtlcs, &maxHtlcsScratch, + ) + + var fundingKeyScratch *btcec.PublicKey + fundingKey := tlv.MakePrimitiveRecord( + DPFundingPubkey, &fundingKeyScratch, + ) + + var chanTypeScratch ChannelType + chanType := tlv.MakeDynamicRecord( + DPChannelType, &chanTypeScratch, chanTypeScratch.featureBitLen, + channelTypeEncoder, channelTypeDecoder, + ) + + var kickoffFeerateScratch uint32 + kickoffFeerate := tlv.MakePrimitiveRecord( + DPKickoffFeerate, &kickoffFeerateScratch, + ) + + // Create set of Records to read TLV bytestream into. + records := []tlv.Record{ + dustLimit, maxValue, reserve, csvDelay, maxHtlcs, fundingKey, + chanType, kickoffFeerate, + } + tlv.SortRecords(records) + + // Read TLV stream into record set. + extraBytesReader := bytes.NewReader(tlvRecords) + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + typeMap, err := tlvStream.DecodeWithParsedTypesP2P(extraBytesReader) + if err != nil { + return err + } + + // Check the results of the TLV Stream decoding and appropriately set + // message fields. + if val, ok := typeMap[DPDustLimitSatoshis]; ok && val == nil { + dp.DustLimit = fn.Some(btcutil.Amount(dustLimitScratch)) + } + if val, ok := typeMap[DPMaxHtlcValueInFlightMsat]; ok && val == nil { + dp.MaxValueInFlight = fn.Some(MilliSatoshi(maxValueScratch)) + } + if val, ok := typeMap[DPChannelReserveSatoshis]; ok && val == nil { + dp.ChannelReserve = fn.Some(btcutil.Amount(reserveScratch)) + } + if val, ok := typeMap[DPToSelfDelay]; ok && val == nil { + dp.CsvDelay = fn.Some(csvDelayScratch) + } + if val, ok := typeMap[DPMaxAcceptedHtlcs]; ok && val == nil { + dp.MaxAcceptedHTLCs = fn.Some(maxHtlcsScratch) + } + if val, ok := typeMap[DPFundingPubkey]; ok && val == nil { + dp.FundingKey = fn.Some(*fundingKeyScratch) + } + if val, ok := typeMap[DPChannelType]; ok && val == nil { + dp.ChannelType = fn.Some(chanTypeScratch) + } + if val, ok := typeMap[DPKickoffFeerate]; ok && val == nil { + dp.KickoffFeerate = fn.Some( + chainfee.SatPerKWeight(kickoffFeerateScratch), + ) + } + + if len(tlvRecords) != 0 { + dp.ExtraData = tlvRecords + } + + return nil +} + +// MsgType returns the MessageType code which uniquely identifies this message +// as a DynPropose on the wire. +// +// This is part of the lnwire.Message interface. +func (dp *DynPropose) MsgType() MessageType { + return MsgDynPropose +} diff --git a/lnwire/dyn_reject.go b/lnwire/dyn_reject.go new file mode 100644 index 000000000..2c6484424 --- /dev/null +++ b/lnwire/dyn_reject.go @@ -0,0 +1,76 @@ +package lnwire + +import ( + "bytes" + "io" +) + +// DynReject is a message that is sent during a dynamic commitments negotiation +// process. It is sent by both parties to propose new channel parameters. +type DynReject struct { + // ChanID identifies the channel whose parameters we are trying to + // re-negotiate. + ChanID ChannelID + + // UpdateRejections is a bit vector that specifies which of the + // DynPropose parameters we wish to call out as being unacceptable. + UpdateRejections RawFeatureVector + + // ExtraData is the set of data that was appended to this message to + // fill out the full maximum transport message size. These fields can + // be used to specify optional data such as custom TLV fields. + // + // NOTE: Since the fields in this structure are part of the TLV stream, + // ExtraData will contain all TLV records _except_ the ones that are + // present in earlier parts of this structure. + ExtraData ExtraOpaqueData +} + +// A compile time check to ensure DynReject implements the lnwire.Message +// interface. +var _ Message = (*DynReject)(nil) + +// Encode serializes the target DynReject into the passed io.Writer. +// Serialization will observe the rules defined by the passed protocol version. +// +// This is a part of the lnwire.Message interface. +func (dr *DynReject) Encode(w *bytes.Buffer, _ uint32) error { + if err := WriteChannelID(w, dr.ChanID); err != nil { + return err + } + + if err := WriteRawFeatureVector(w, &dr.UpdateRejections); err != nil { + return err + } + + return WriteBytes(w, dr.ExtraData) +} + +// Decode deserializes the serialized DynReject stored in the passed io.Reader +// into the target DynReject using the deserialization rules defined by the +// passed protocol version. +// +// This is a part of the lnwire.Message interface. +func (dr *DynReject) Decode(r io.Reader, _ uint32) error { + var extra ExtraOpaqueData + + if err := ReadElements( + r, &dr.ChanID, &dr.UpdateRejections, &extra, + ); err != nil { + return err + } + + if len(extra) != 0 { + dr.ExtraData = extra + } + + return nil +} + +// MsgType returns the MessageType code which uniquely identifies this message +// as a DynReject on the wire. +// +// This is part of the lnwire.Message interface. +func (dr *DynReject) MsgType() MessageType { + return MsgDynReject +} diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go index f789e7ce7..f364867a7 100644 --- a/lnwire/fuzz_test.go +++ b/lnwire/fuzz_test.go @@ -577,6 +577,50 @@ func FuzzUpdateFulfillHTLC(f *testing.F) { }) } +func FuzzDynPropose(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + // Prefix with DynPropose. + data = prefixWithMsgType(data, MsgDynPropose) + + // Pass the message into our general fuzz harness for wire + // messages! + harness(t, data) + }) +} + +func FuzzDynReject(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + // Prefix with DynReject. + data = prefixWithMsgType(data, MsgDynReject) + + // Pass the message into our general fuzz harness for wire + // messages! + harness(t, data) + }) +} + +func FuzzDynAck(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + // Prefix with DynReject. + data = prefixWithMsgType(data, MsgDynAck) + + // Pass the message into our general fuzz harness for wire + // messages! + harness(t, data) + }) +} + +func FuzzKickoffSig(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + // Prefix with KickoffSig + data = prefixWithMsgType(data, MsgKickoffSig) + + // Pass the message into our general fuzz harness for wire + // messages! + harness(t, data) + }) +} + func FuzzCustomMessage(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte, customMessageType uint16) { if customMessageType < uint16(CustomTypeStart) { diff --git a/lnwire/kickoff_sig.go b/lnwire/kickoff_sig.go new file mode 100644 index 000000000..3e46db453 --- /dev/null +++ b/lnwire/kickoff_sig.go @@ -0,0 +1,56 @@ +package lnwire + +import ( + "bytes" + "io" +) + +// KickoffSig is the message used to transmit the signature for a kickoff +// transaction during the execution phase of a dynamic commitment negotiation +// that requires a reanchoring step. +type KickoffSig struct { + // ChanID identifies the channel id for which this signature is + // intended. + ChanID ChannelID + + // Signature contains the ECDSA signature that signs the kickoff + // transaction. + Signature Sig + + // ExtraData is the set of data that was appended to this message to + // fill out the full maximum transport message size. These fields can + // be used to specify optional data such as custom TLV fields. + ExtraData ExtraOpaqueData +} + +// A compile time check to ensure that KickoffSig implements the lnwire.Message +// interface. +var _ Message = (*KickoffSig)(nil) + +// Encode serializes the target KickoffSig into the passed bytes.Buffer +// observing the specified protocol version. +// +// This is part of the lnwire.Message interface. +func (ks *KickoffSig) Encode(w *bytes.Buffer, _ uint32) error { + if err := WriteChannelID(w, ks.ChanID); err != nil { + return err + } + if err := WriteSig(w, ks.Signature); err != nil { + return err + } + + return WriteBytes(w, ks.ExtraData) +} + +// Decode deserializes a serialized KickoffSig message stored in the passed +// io.Reader observing the specified protocol version. +// +// This is part of the lnwire.Message interface. +func (ks *KickoffSig) Decode(r io.Reader, _ uint32) error { + return ReadElements(r, &ks.ChanID, &ks.Signature, &ks.ExtraData) +} + +// MsgType returns the integer uniquely identifying KickoffSig on the wire. +// +// This is part of the lnwire.Message interface. +func (ks *KickoffSig) MsgType() MessageType { return MsgKickoffSig } diff --git a/lnwire/lnwire.go b/lnwire/lnwire.go index 46257cce5..50a547e22 100644 --- a/lnwire/lnwire.go +++ b/lnwire/lnwire.go @@ -591,6 +591,14 @@ func ReadElement(r io.Reader, element interface{}) error { } *e = pubKey + case *RawFeatureVector: + f := NewRawFeatureVector() + err = f.Decode(r) + if err != nil { + return err + } + *e = *f + case **RawFeatureVector: f := NewRawFeatureVector() err = f.Decode(r) diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index f5c028581..9d92b970b 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -20,6 +20,8 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/tor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -708,6 +710,118 @@ func TestLightningWireProtocol(t *testing.T) { v[0] = reflect.ValueOf(req) }, + MsgDynPropose: func(v []reflect.Value, r *rand.Rand) { + var dp DynPropose + rand.Read(dp.ChanID[:]) + + if rand.Uint32()%2 == 0 { + v := btcutil.Amount(rand.Uint32()) + dp.DustLimit = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v := MilliSatoshi(rand.Uint32()) + dp.MaxValueInFlight = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v := btcutil.Amount(rand.Uint32()) + dp.ChannelReserve = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v := uint16(rand.Uint32()) + dp.CsvDelay = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v := uint16(rand.Uint32()) + dp.MaxAcceptedHTLCs = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v, _ := btcec.NewPrivateKey() + dp.FundingKey = fn.Some(*v.PubKey()) + } + + if rand.Uint32()%2 == 0 { + v := ChannelType(*NewRawFeatureVector()) + dp.ChannelType = fn.Some(v) + } + + if rand.Uint32()%2 == 0 { + v := chainfee.SatPerKWeight(rand.Uint32()) + dp.KickoffFeerate = fn.Some(v) + } + + v[0] = reflect.ValueOf(dp) + }, + MsgDynReject: func(v []reflect.Value, r *rand.Rand) { + var dr DynReject + rand.Read(dr.ChanID[:]) + + features := NewRawFeatureVector() + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPDustLimitSatoshis)) + } + + if rand.Uint32()%2 == 0 { + features.Set( + FeatureBit(DPMaxHtlcValueInFlightMsat), + ) + } + + if rand.Uint32()%2 == 0 { + features.Set( + FeatureBit(DPChannelReserveSatoshis), + ) + } + + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPToSelfDelay)) + } + + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPMaxAcceptedHtlcs)) + } + + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPFundingPubkey)) + } + + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPChannelType)) + } + + if rand.Uint32()%2 == 0 { + features.Set(FeatureBit(DPKickoffFeerate)) + } + dr.UpdateRejections = *features + + v[0] = reflect.ValueOf(dr) + }, + MsgDynAck: func(v []reflect.Value, r *rand.Rand) { + var da DynAck + + rand.Read(da.ChanID[:]) + if rand.Uint32()%2 == 0 { + var nonce Musig2Nonce + rand.Read(nonce[:]) + da.LocalNonce = fn.Some(nonce) + } + + v[0] = reflect.ValueOf(da) + }, + MsgKickoffSig: func(v []reflect.Value, r *rand.Rand) { + ks := KickoffSig{ + ExtraData: make([]byte, 0), + } + + rand.Read(ks.ChanID[:]) + rand.Read(ks.Signature.bytes[:]) + + v[0] = reflect.ValueOf(ks) + }, MsgCommitSig: func(v []reflect.Value, r *rand.Rand) { req := NewCommitSig() if _, err := r.Read(req.ChanID[:]); err != nil { @@ -1153,6 +1267,30 @@ func TestLightningWireProtocol(t *testing.T) { return mainScenario(&m) }, }, + { + msgType: MsgDynPropose, + scenario: func(m DynPropose) bool { + return mainScenario(&m) + }, + }, + { + msgType: MsgDynReject, + scenario: func(m DynReject) bool { + return mainScenario(&m) + }, + }, + { + msgType: MsgDynAck, + scenario: func(m DynAck) bool { + return mainScenario(&m) + }, + }, + { + msgType: MsgKickoffSig, + scenario: func(m KickoffSig) bool { + return mainScenario(&m) + }, + }, { msgType: MsgUpdateAddHTLC, scenario: func(m UpdateAddHTLC) bool { diff --git a/lnwire/message.go b/lnwire/message.go index 02447b806..bc79ed003 100644 --- a/lnwire/message.go +++ b/lnwire/message.go @@ -34,6 +34,9 @@ const ( MsgChannelReady = 36 MsgShutdown = 38 MsgClosingSigned = 39 + MsgDynPropose = 111 + MsgDynAck = 113 + MsgDynReject = 115 MsgUpdateAddHTLC = 128 MsgUpdateFulfillHTLC = 130 MsgUpdateFailHTLC = 131 @@ -51,6 +54,7 @@ const ( MsgQueryChannelRange = 263 MsgReplyChannelRange = 264 MsgGossipTimestampRange = 265 + MsgKickoffSig = 777 ) // ErrorEncodeMessage is used when failed to encode the message payload. @@ -94,6 +98,14 @@ func (t MessageType) String() string { return "Shutdown" case MsgClosingSigned: return "ClosingSigned" + case MsgDynPropose: + return "DynPropose" + case MsgDynAck: + return "DynAck" + case MsgDynReject: + return "DynReject" + case MsgKickoffSig: + return "KickoffSig" case MsgUpdateAddHTLC: return "UpdateAddHTLC" case MsgUpdateFailHTLC: @@ -196,6 +208,14 @@ func makeEmptyMessage(msgType MessageType) (Message, error) { msg = &Shutdown{} case MsgClosingSigned: msg = &ClosingSigned{} + case MsgDynPropose: + msg = &DynPropose{} + case MsgDynAck: + msg = &DynAck{} + case MsgDynReject: + msg = &DynReject{} + case MsgKickoffSig: + msg = &KickoffSig{} case MsgUpdateAddHTLC: msg = &UpdateAddHTLC{} case MsgUpdateFailHTLC: