From b298c84d2171f029195c19f4ea889f79ca05f90b Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 27 Oct 2023 12:59:07 -0700 Subject: [PATCH 1/6] fn: introduce option type this commit introduces many of the most common functions you will want to use with the Option type. Not all of them are used immediately in this PR. --- fn/option.go | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 fn/option.go diff --git a/fn/option.go b/fn/option.go new file mode 100644 index 0000000000..a2c3afdc25 --- /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 +} From e5f7ed8ba179cfeea5d931b27bfabe06ec04d327 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 22 Sep 2023 19:26:52 -0700 Subject: [PATCH 2/6] lnwire: introduce message types for dynamic commitment negotiation lnwire: add tests for dynamic commitment wire serialization --- lnwire/dyn_ack.go | 53 +++++++ lnwire/dyn_propose.go | 319 ++++++++++++++++++++++++++++++++++++++++++ lnwire/dyn_reject.go | 76 ++++++++++ lnwire/lnwire.go | 8 ++ lnwire/lnwire_test.go | 104 ++++++++++++++ lnwire/message.go | 15 ++ 6 files changed, 575 insertions(+) create mode 100644 lnwire/dyn_ack.go create mode 100644 lnwire/dyn_propose.go create mode 100644 lnwire/dyn_reject.go diff --git a/lnwire/dyn_ack.go b/lnwire/dyn_ack.go new file mode 100644 index 0000000000..d0b72e030c --- /dev/null +++ b/lnwire/dyn_ack.go @@ -0,0 +1,53 @@ +package lnwire + +import ( + "bytes" + "io" +) + +// 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 + + // 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 + } + + 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 { + return ReadElements(r, &da.ChanID, &da.ExtraData) +} + +// 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 0000000000..b0cc1198e9 --- /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 0000000000..2c6484424e --- /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/lnwire.go b/lnwire/lnwire.go index 46257cce51..50a547e22f 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 f5c028581b..00d4c4a9fe 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,96 @@ 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) + }, MsgCommitSig: func(v []reflect.Value, r *rand.Rand) { req := NewCommitSig() if _, err := r.Read(req.ChanID[:]); err != nil { @@ -1153,6 +1245,18 @@ 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: MsgUpdateAddHTLC, scenario: func(m UpdateAddHTLC) bool { diff --git a/lnwire/message.go b/lnwire/message.go index 02447b806c..2f6d64a727 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 @@ -94,6 +97,12 @@ func (t MessageType) String() string { return "Shutdown" case MsgClosingSigned: return "ClosingSigned" + case MsgDynPropose: + return "DynPropose" + case MsgDynAck: + return "DynAck" + case MsgDynReject: + return "DynReject" case MsgUpdateAddHTLC: return "UpdateAddHTLC" case MsgUpdateFailHTLC: @@ -196,6 +205,12 @@ 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 MsgUpdateAddHTLC: msg = &UpdateAddHTLC{} case MsgUpdateFailHTLC: From 3e84d22eebc0b382191c3e111bdbe49221b38d76 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 13 Oct 2023 18:32:52 -0600 Subject: [PATCH 3/6] lnwire: add TLV extension to channel_reestablish for dynamic commitments --- lnwire/channel_reestablish.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lnwire/channel_reestablish.go b/lnwire/channel_reestablish.go index 1b6cfdffc3..b4a5258c8e 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 From 564bf852bb534d2365254dd5c9a21e7cfda6cb01 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Sat, 14 Oct 2023 12:00:30 -0600 Subject: [PATCH 4/6] lnwire: add fuzz tests for new dynamic commitment message types --- lnwire/fuzz_test.go | 33 +++++++++++++++++++++++++++++++++ lnwire/lnwire_test.go | 13 +++++++++++++ 2 files changed, 46 insertions(+) diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go index f789e7ce72..b2ee8471fe 100644 --- a/lnwire/fuzz_test.go +++ b/lnwire/fuzz_test.go @@ -577,6 +577,39 @@ 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 FuzzCustomMessage(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte, customMessageType uint16) { if customMessageType < uint16(CustomTypeStart) { diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index 00d4c4a9fe..873b35709e 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -800,6 +800,13 @@ func TestLightningWireProtocol(t *testing.T) { v[0] = reflect.ValueOf(dr) }, + MsgDynAck: func(v []reflect.Value, r *rand.Rand) { + var da DynAck + + rand.Read(da.ChanID[:]) + + v[0] = reflect.ValueOf(da) + }, MsgCommitSig: func(v []reflect.Value, r *rand.Rand) { req := NewCommitSig() if _, err := r.Read(req.ChanID[:]); err != nil { @@ -1257,6 +1264,12 @@ func TestLightningWireProtocol(t *testing.T) { return mainScenario(&m) }, }, + { + msgType: MsgDynAck, + scenario: func(m DynAck) bool { + return mainScenario(&m) + }, + }, { msgType: MsgUpdateAddHTLC, scenario: func(m UpdateAddHTLC) bool { From 9b1c04c210c5924a211feaef515149fd3c3c93a7 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Thu, 12 Oct 2023 16:56:50 -0600 Subject: [PATCH 5/6] docs: update release notes --- docs/release-notes/release-notes-0.18.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index 948c166bd7..7a288a4713 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 From 9793fbb94bc6cc05b6fbc4758c85e8486872868f Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 18 Oct 2023 20:28:51 -0700 Subject: [PATCH 6/6] lnwire: add musig2 taproot execution messages for dynamic commitments --- lnwire/dyn_ack.go | 87 ++++++++++++++++++++++++++++++++++++++++++- lnwire/fuzz_test.go | 11 ++++++ lnwire/kickoff_sig.go | 56 ++++++++++++++++++++++++++++ lnwire/lnwire_test.go | 21 +++++++++++ lnwire/message.go | 5 +++ 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 lnwire/kickoff_sig.go diff --git a/lnwire/dyn_ack.go b/lnwire/dyn_ack.go index d0b72e030c..24f23a228d 100644 --- a/lnwire/dyn_ack.go +++ b/lnwire/dyn_ack.go @@ -3,6 +3,17 @@ 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 @@ -13,6 +24,13 @@ type DynAck struct { // 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. @@ -32,6 +50,30 @@ func (da *DynAck) Encode(w *bytes.Buffer, _ uint32) error { 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) } @@ -41,7 +83,50 @@ func (da *DynAck) Encode(w *bytes.Buffer, _ uint32) error { // // This is a part of the lnwire.Message interface. func (da *DynAck) Decode(r io.Reader, _ uint32) error { - return ReadElements(r, &da.ChanID, &da.ExtraData) + // 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 diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go index b2ee8471fe..f364867a70 100644 --- a/lnwire/fuzz_test.go +++ b/lnwire/fuzz_test.go @@ -610,6 +610,17 @@ func FuzzDynAck(f *testing.F) { }) } +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 0000000000..3e46db453f --- /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_test.go b/lnwire/lnwire_test.go index 873b35709e..9d92b970b5 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -804,9 +804,24 @@ func TestLightningWireProtocol(t *testing.T) { 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 { @@ -1270,6 +1285,12 @@ func TestLightningWireProtocol(t *testing.T) { 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 2f6d64a727..bc79ed003d 100644 --- a/lnwire/message.go +++ b/lnwire/message.go @@ -54,6 +54,7 @@ const ( MsgQueryChannelRange = 263 MsgReplyChannelRange = 264 MsgGossipTimestampRange = 265 + MsgKickoffSig = 777 ) // ErrorEncodeMessage is used when failed to encode the message payload. @@ -103,6 +104,8 @@ func (t MessageType) String() string { return "DynAck" case MsgDynReject: return "DynReject" + case MsgKickoffSig: + return "KickoffSig" case MsgUpdateAddHTLC: return "UpdateAddHTLC" case MsgUpdateFailHTLC: @@ -211,6 +214,8 @@ func makeEmptyMessage(msgType MessageType) (Message, error) { msg = &DynAck{} case MsgDynReject: msg = &DynReject{} + case MsgKickoffSig: + msg = &KickoffSig{} case MsgUpdateAddHTLC: msg = &UpdateAddHTLC{} case MsgUpdateFailHTLC: