From 42089b19d6e0f86a7be82567942f80ca56c2fd53 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 25 Jun 2025 00:42:33 +0800 Subject: [PATCH] lnwire: add DynCommit message to match spec --- lnwire/dyn_commit.go | 247 +++++++++++++++++++++++++++++++++++++++++++ lnwire/fuzz_test.go | 6 ++ lnwire/message.go | 5 + 3 files changed, 258 insertions(+) create mode 100644 lnwire/dyn_commit.go diff --git a/lnwire/dyn_commit.go b/lnwire/dyn_commit.go new file mode 100644 index 000000000..551598379 --- /dev/null +++ b/lnwire/dyn_commit.go @@ -0,0 +1,247 @@ +package lnwire + +import ( + "bytes" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/tlv" +) + +// DynCommit is a composite message that is used to irrefutably execute a +// dynamic commitment update. +type DynCommit struct { + // DynPropose is an embedded version of the original DynPropose message + // that initiated this negotiation. + DynPropose + + // DynAck is an embedded version of the original DynAck message that + // countersigned this negotiation. + DynAck + + // 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 = (*DynCommit)(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 (dc *DynCommit) Encode(w *bytes.Buffer, _ uint32) error { + if err := WriteChannelID(w, dc.DynPropose.ChanID); err != nil { + return err + } + + if err := WriteSig(w, dc.Sig); err != nil { + return err + } + + var tlvRecords []tlv.Record + dc.DustLimit.WhenSome(func(dl btcutil.Amount) { + protoSats := uint64(dl) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPDustLimitSatoshis, &protoSats, + ), + ) + }) + dc.MaxValueInFlight.WhenSome(func(max MilliSatoshi) { + protoSats := uint64(max) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPMaxHtlcValueInFlightMsat, &protoSats, + ), + ) + }) + dc.ChannelReserve.WhenSome(func(min btcutil.Amount) { + channelReserve := uint64(min) + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPChannelReserveSatoshis, &channelReserve, + ), + ) + }) + dc.CsvDelay.WhenSome(func(wait uint16) { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPToSelfDelay, &wait, + ), + ) + }) + dc.MaxAcceptedHTLCs.WhenSome(func(max uint16) { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPMaxAcceptedHtlcs, &max, + ), + ) + }) + dc.FundingKey.WhenSome(func(key btcec.PublicKey) { + keyScratch := &key + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + DPFundingPubkey, &keyScratch, + ), + ) + }) + dc.ChannelType.WhenSome(func(ty ChannelType) { + tlvRecords = append( + tlvRecords, tlv.MakeDynamicRecord( + DPChannelType, &ty, + ty.featureBitLen, + channelTypeEncoder, channelTypeDecoder, + ), + ) + }) + dc.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 + } + + dc.ExtraData = ExtraOpaqueData(extraBytesWriter.Bytes()) + + return WriteBytes(w, dc.ExtraData) +} + +// Decode deserializes the serialized DynCommit 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 (dc *DynCommit) Decode(r io.Reader, _ uint32) error { + // Parse out main message. + if err := ReadElements(r, &dc.DynPropose.ChanID, &dc.Sig); err != nil { + return err + } + dc.DynAck.ChanID = dc.DynPropose.ChanID + + // 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 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 { + dc.DustLimit = fn.Some(btcutil.Amount(dustLimitScratch)) + } + if val, ok := typeMap[DPMaxHtlcValueInFlightMsat]; ok && val == nil { + dc.MaxValueInFlight = fn.Some(MilliSatoshi(maxValueScratch)) + } + if val, ok := typeMap[DPChannelReserveSatoshis]; ok && val == nil { + dc.ChannelReserve = fn.Some(btcutil.Amount(reserveScratch)) + } + if val, ok := typeMap[DPToSelfDelay]; ok && val == nil { + dc.CsvDelay = fn.Some(csvDelayScratch) + } + if val, ok := typeMap[DPMaxAcceptedHtlcs]; ok && val == nil { + dc.MaxAcceptedHTLCs = fn.Some(maxHtlcsScratch) + } + if val, ok := typeMap[DPFundingPubkey]; ok && val == nil { + dc.FundingKey = fn.Some(*fundingKeyScratch) + } + if val, ok := typeMap[DPChannelType]; ok && val == nil { + dc.ChannelType = fn.Some(chanTypeScratch) + } + if val, ok := typeMap[DPKickoffFeerate]; ok && val == nil { + dc.KickoffFeerate = fn.Some( + chainfee.SatPerKWeight(kickoffFeerateScratch), + ) + } + + if len(tlvRecords) != 0 { + dc.ExtraData = tlvRecords + } + + return nil +} + +// MsgType returns the MessageType code which uniquely identifies this message +// as a DynCommit on the wire. +// +// This is part of the lnwire.Message interface. +func (dc *DynCommit) MsgType() MessageType { + return MsgDynCommit +} diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go index e608eb9b0..054af585c 100644 --- a/lnwire/fuzz_test.go +++ b/lnwire/fuzz_test.go @@ -452,6 +452,12 @@ func FuzzDynAck(f *testing.F) { }) } +func FuzzDynCommit(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + wireMsgHarness(t, data, MsgDynCommit) + }) +} + func FuzzKickoffSig(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { wireMsgHarness(t, data, MsgKickoffSig) diff --git a/lnwire/message.go b/lnwire/message.go index ea480075a..a36359f0b 100644 --- a/lnwire/message.go +++ b/lnwire/message.go @@ -44,6 +44,7 @@ const ( MsgDynPropose = 111 MsgDynAck = 113 MsgDynReject = 115 + MsgDynCommit = 117 MsgUpdateAddHTLC = 128 MsgUpdateFulfillHTLC = 130 MsgUpdateFailHTLC = 131 @@ -140,6 +141,8 @@ func (t MessageType) String() string { return "DynAck" case MsgDynReject: return "DynReject" + case MsgDynCommit: + return "DynCommit" case MsgKickoffSig: return "KickoffSig" case MsgUpdateAddHTLC: @@ -300,6 +303,8 @@ func makeEmptyMessage(msgType MessageType) (Message, error) { msg = &DynAck{} case MsgDynReject: msg = &DynReject{} + case MsgDynCommit: + msg = &DynCommit{} case MsgKickoffSig: msg = &KickoffSig{} case MsgUpdateAddHTLC: