From 6947e0a87a76b5b169a0ff6d100f75912dbe4943 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 23 May 2025 22:37:33 +0800 Subject: [PATCH] channeldb: add customized encoding for `HtlcIndex` --- channeldb/revocation_log.go | 72 ++++++++++++++++++++++++++++++-- channeldb/revocation_log_test.go | 61 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 840d5c502..5a7f7a76b 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -2,6 +2,7 @@ package channeldb import ( "bytes" + "encoding/binary" "errors" "io" "math" @@ -512,13 +513,22 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { // deserializeHTLCEntries deserializes a list of HTLC entries based on tlv // format. func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { - var htlcs []*HTLCEntry + var ( + htlcs []*HTLCEntry + + // htlcIndexBlob defines the tlv record type to be used when + // decoding from the disk. We use it instead of the one defined + // in `HTLCEntry.HtlcIndex` as previously this field was encoded + // using `uint16`, thus we will read it as raw bytes and + // deserialize it further below. + htlcIndexBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] + ) for { var htlc HTLCEntry customBlob := htlc.CustomBlob.Zero() - htlcIndex := htlc.HtlcIndex.Zero() + htlcIndex := htlcIndexBlob.Zero() // Create the tlv stream. records := []tlv.Record{ @@ -551,7 +561,14 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { } if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil { - htlc.HtlcIndex = tlv.SomeRecordT(htlcIndex) + record, err := deserializeHtlcIndexCompatible( + htlcIndex.Val, + ) + if err != nil { + return nil, err + } + + htlc.HtlcIndex = record } // Append the entry. @@ -561,6 +578,55 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { return htlcs, nil } +// deserializeHtlcIndexCompatible takes raw bytes and decodes it into an +// optional record that's assigned to the entry's HtlcIndex. +// +// NOTE: previously this `HtlcIndex` was a tlv record that used `uint16` to +// encode its value. Given now its value is encoded using BigSizeT, and for any +// BigSizeT, its possible length values are 1, 3, 5, and 8. This means if the +// tlv record has a length of 2, we know for sure it must be an old record +// whose value was encoded using uint16. +func deserializeHtlcIndexCompatible(rawBytes []byte) ( + tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]], error) { + + var ( + // record defines the record that's used by the HtlcIndex in the + // entry. + record tlv.OptionalRecordT[ + tlv.TlvType6, tlv.BigSizeT[uint64], + ] + + // htlcIndexVal is the decoded uint64 value. + htlcIndexVal uint64 + ) + + // If the length of the tlv record is 2, it must be encoded using uint16 + // as the BigSizeT encoding cannot have this length. + if len(rawBytes) == 2 { + // Decode the raw bytes into uint16 and convert it into uint64. + htlcIndexVal = uint64(binary.BigEndian.Uint16(rawBytes)) + } else { + // This value is encoded using BigSizeT, we now use the decoder + // to deserialize the raw bytes. + r := bytes.NewBuffer(rawBytes) + + // Create a buffer to be used in the decoding process. + buf := [8]byte{} + + // Use the BigSizeT's decoder. + err := tlv.DBigSize(r, &htlcIndexVal, &buf, 8) + if err != nil { + return record, err + } + } + + record = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( + tlv.NewBigSizeT(htlcIndexVal), + )) + + return record, nil +} + // writeTlvStream is a helper function that encodes the tlv stream into the // writer. func writeTlvStream(w io.Writer, s *tlv.Stream) error { diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index 626d23b34..ceec9a265 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -2,6 +2,7 @@ package channeldb import ( "bytes" + "encoding/binary" "io" "math" "math/rand" @@ -785,3 +786,63 @@ func createTestRevocationLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket, return chanBucket, logBucket, nil } + +// TestDeserializeHTLCEntriesLegacy checks that the legacy encoding of the +// HtlcIndex can be correctly read as BigSizeT. The field `HtlcIndex` was +// encoded using `uint16` and should now be deserialized into BigSizeT +// on-the-fly. +func TestDeserializeHTLCEntriesLegacy(t *testing.T) { + t.Parallel() + + // rawBytes defines the bytes read from the disk for the testing HTLC + // entry. + rawBytes := []byte{ + // Body length 45. + 0x2d, + // Rhash tlv. + 0x0, 0x0, + // RefundTimeout tlv. + 0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0, + // OutputIndex tlv. + 0x2, 0x2, 0x0, 0xa, + // Incoming tlv. + 0x3, 0x1, 0x1, + // Amt tlv. + 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, + // Custom blob tlv. + 0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, + + // HTLC index tlv. + // + // NOTE: We are missing two bytes in the end, which is appended + // below. + 0x6, 0x2, + } + + // Iterate through all possible values encoded using uint16. They should + // be correctly read as uint16 and converted to BigSizeT. + for i := range math.MaxUint16 + 1 { + // Encode the index using two bytes. + rawHtlcIndex := make([]byte, 2) + binary.BigEndian.PutUint16(rawHtlcIndex, uint16(i)) + + // Copy the raw bytes and append the htlc index. + rawEntry := bytes.Clone(rawBytes) + rawEntry = append(rawEntry, rawHtlcIndex...) + + // Read the tlv stream. + buf := bytes.NewBuffer(rawEntry) + htlcs, err := deserializeHTLCEntries(buf) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Len(t, htlcs, 1) + + // Assert that the uint16 is converted to BigSizeT. + record := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( + tlv.NewBigSizeT(uint64(i)), + )) + require.Equal(t, record, htlcs[0].HtlcIndex) + } +}