diff --git a/channeldb/payments.go b/channeldb/payments.go index a0db8f22a..0ccb75774 100644 --- a/channeldb/payments.go +++ b/channeldb/payments.go @@ -1096,6 +1096,25 @@ func serializeHTLCAttemptInfo(w io.Writer, a *HTLCAttemptInfo) error { return err } + // Merge the fixed/known records together with the custom records to + // serialize them as a single blob. We can't do this in SerializeRoute + // because we're in the middle of the byte stream there. We can only do + // TLV serialization at the end of the stream, since EOF is allowed for + // a stream if no more data is expected. + producers := []tlv.RecordProducer{ + &a.Route.FirstHopAmount, + } + tlvData, err := lnwire.MergeAndEncode( + producers, nil, a.Route.FirstHopWireCustomRecords, + ) + if err != nil { + return err + } + + if _, err := w.Write(tlvData); err != nil { + return err + } + return nil } @@ -1133,6 +1152,22 @@ func deserializeHTLCAttemptInfo(r io.Reader) (*HTLCAttemptInfo, error) { a.Hash = &hash + // Read any remaining data (if any) and parse it into the known records + // and custom records. + extraData, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + customRecords, _, _, err := lnwire.ParseAndExtractCustomRecords( + extraData, &a.Route.FirstHopAmount, + ) + if err != nil { + return nil, err + } + + a.Route.FirstHopWireCustomRecords = customRecords + return a, nil } @@ -1398,6 +1433,8 @@ func SerializeRoute(w io.Writer, r route.Route) error { } } + // Any new/extra TLV data is encoded in serializeHTLCAttemptInfo! + return nil } @@ -1431,5 +1468,7 @@ func DeserializeRoute(r io.Reader) (route.Route, error) { } rt.Hops = hops + // Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo! + return rt, nil } diff --git a/channeldb/payments_test.go b/channeldb/payments_test.go index 844b818e8..0c3753e66 100644 --- a/channeldb/payments_test.go +++ b/channeldb/payments_test.go @@ -16,6 +16,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -149,20 +150,34 @@ func TestSentPaymentSerialization(t *testing.T) { require.NoError(t, err, "deserialize") require.Equal(t, c, newCreationInfo) + b.Reset() require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize") newWireInfo, err := deserializeHTLCAttemptInfo(&b) require.NoError(t, err, "deserialize") - newWireInfo.AttemptID = s.AttemptID - // First we verify all the records match up properly, as they aren't - // able to be properly compared using reflect.DeepEqual. - err = assertRouteEqual(&s.Route, &newWireInfo.Route) - require.NoError(t, err) + // First we verify all the records match up properly. + require.Equal(t, s.Route, newWireInfo.Route) + + // We now add the new fields and custom records to the route and + // serialize it again. + b.Reset() + s.Route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0]( + tlv.NewBigSizeT(lnwire.MilliSatoshi(1234)), + ) + s.Route.FirstHopWireCustomRecords = lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType + 3: []byte{4, 5, 6}, + } + require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize") + + newWireInfo, err = deserializeHTLCAttemptInfo(&b) + require.NoError(t, err, "deserialize") + require.Equal(t, s.Route, newWireInfo.Route) // Clear routes to allow DeepEqual to compare the remaining fields. newWireInfo.Route = route.Route{} s.Route = route.Route{} + newWireInfo.AttemptID = s.AttemptID // Call session key method to set our cached session key so we can use // DeepEqual, and assert that our key equals the original key. diff --git a/routing/route/route.go b/routing/route/route.go index 0516cc7a2..9aa28759b 100644 --- a/routing/route/route.go +++ b/routing/route/route.go @@ -488,6 +488,26 @@ type Route struct { // Hops contains details concerning the specific forwarding details at // each hop. Hops []*Hop + + // FirstHopAmount is the amount that should actually be sent to the + // first hop in the route. This is only different from TotalAmount above + // for custom channels where the on-chain amount doesn't necessarily + // reflect all the value of an outgoing payment. + FirstHopAmount tlv.RecordT[ + tlv.TlvType0, tlv.BigSizeT[lnwire.MilliSatoshi], + ] + + // FirstHopWireCustomRecords is a set of custom records that should be + // included in the wire message sent to the first hop. This is only set + // on custom channels and is used to include additional information + // about the actual value of the payment. + // + // NOTE: Since these records already represent TLV records, and we + // enforce them to be in the custom range (e.g. >= 65536), we don't use + // another parent record type here. Instead, when serializing the Route + // we merge the TLV records together with the custom records and encode + // everything as a single TLV stream. + FirstHopWireCustomRecords lnwire.CustomRecords } // Copy returns a deep copy of the Route.