diff --git a/lnwire/dyn_ack.go b/lnwire/dyn_ack.go new file mode 100644 index 000000000..d0b72e030 --- /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 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/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..00d4c4a9f 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 02447b806..2f6d64a72 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: