diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 61a6521e0..64e22d069 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/tlv" ) @@ -43,6 +44,16 @@ var ( // HTLCEntry specifies the minimal info needed to be stored on disk for ALL the // historical HTLCs, which is useful for constructing RevocationLog when a // breach is detected. +// The actual size of each HTLCEntry varies based on its RHash and Amt(sat), +// summarized as follows, +// +// | RHash empty | Amt<=252 | Amt<=65,535 | Amt<=4,294,967,295 | otherwise | +// |:-----------:|:--------:|:-----------:|:------------------:|:---------:| +// | true | 19 | 21 | 23 | 26 | +// | false | 51 | 53 | 55 | 58 | +// +// So the size varies from 19 bytes to 58 bytes, where most likely to be 23 or +// 55 bytes. // // NOTE: all the fields saved to disk use the primitive go types so they can be // made into tlv records without further conversion. @@ -86,6 +97,46 @@ type HTLCEntry struct { incomingTlv uint8 } +// RHashLen is used by MakeDynamicRecord to return the size of the RHash. +// +// NOTE: for zero hash, we return a length 0. +func (h *HTLCEntry) RHashLen() uint64 { + if h.RHash == lntypes.ZeroHash { + return 0 + } + return 32 +} + +// RHashEncoder is the customized encoder which skips encoding the empty hash. +func RHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + v, ok := val.(*[32]byte) + if !ok { + return tlv.NewTypeForEncodingErr(val, "RHash") + } + + // If the value is an empty hash, we will skip encoding it. + if *v == lntypes.ZeroHash { + return nil + } + + return tlv.EBytes32(w, v, buf) +} + +// RHashDecoder is the customized decoder which skips decoding the empty hash. +func RHashDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + v, ok := val.(*[32]byte) + if !ok { + return tlv.NewTypeForEncodingErr(val, "RHash") + } + + // If the length is zero, we will skip encoding the empty hash. + if l == 0 { + return nil + } + + return tlv.DBytes32(r, v, buf, 32) +} + // toTlvStream converts an HTLCEntry record into a tlv representation. func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { const ( @@ -103,7 +154,10 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { ) return tlv.NewStream( - tlv.MakePrimitiveRecord(rHashType, &h.RHash), + tlv.MakeDynamicRecord( + rHashType, &h.RHash, h.RHashLen, + RHashEncoder, RHashDecoder, + ), tlv.MakePrimitiveRecord( refundTimeoutType, &h.RefundTimeout, ), @@ -111,7 +165,9 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { outputIndexType, &h.OutputIndex, ), tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv), - tlv.MakePrimitiveRecord(amtType, &h.amtTlv), + // We will save 3 bytes if the amount is less or equal to + // 4,294,967,295 msat, or roughly 0.043 bitcoin. + tlv.MakeBigSizeRecord(amtType, &h.amtTlv), ) } diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index 93ec784b2..1b17c9b28 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -34,30 +34,26 @@ var ( } testHTLCEntry = HTLCEntry{ - RefundTimeout: 100, + RefundTimeout: 740_000, OutputIndex: 10, Incoming: true, - Amt: 255, - amtTlv: 255, + Amt: 1000_000, + amtTlv: 1000_000, incomingTlv: 1, } testHTLCEntryBytes = []byte{ - // Body length 58. - 0x39, + // Body length 23. + 0x16, // Rhash tlv. - 0x0, 0x20, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, // RefundTimeout tlv. - 0x1, 0x4, 0x0, 0x0, 0x0, 0x64, + 0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0, // OutputIndex tlv. 0x2, 0x2, 0x0, 0xa, // Incoming tlv. 0x3, 0x1, 0x1, // Amt tlv. - 0x4, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, + 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, } testChannelCommit = ChannelCommitment{ @@ -161,7 +157,7 @@ func TestReadTLVStreamErr(t *testing.T) { require.Zero(t, valueRead) } -func TestSerializeHTLCEntries(t *testing.T) { +func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) { t.Parallel() // Copy the testHTLCEntry. @@ -181,6 +177,37 @@ func TestSerializeHTLCEntries(t *testing.T) { require.Equal(t, testHTLCEntryBytes, buf.Bytes()) } +func TestSerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Copy the testHTLCEntry. + entry := testHTLCEntry + + // Create a fake rHash. + rHashBytes := bytes.Repeat([]byte{10}, 32) + copy(entry.RHash[:], rHashBytes) + + // Construct the serialized bytes. + // + // Exclude the first 3 bytes, which are total length, RHash type and + // RHash length(0). + partialBytes := testHTLCEntryBytes[3:] + + // Write the total length and RHash tlv. + expectedBytes := []byte{0x36, 0x0, 0x20} + expectedBytes = append(expectedBytes, rHashBytes...) + + // Append the rest. + expectedBytes = append(expectedBytes, partialBytes...) + + buf := bytes.NewBuffer([]byte{}) + err := serializeHTLCEntries(buf, []*HTLCEntry{&entry}) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Equal(t, expectedBytes, buf.Bytes()) +} + func TestSerializeRevocationLog(t *testing.T) { t.Parallel() @@ -197,7 +224,7 @@ func TestSerializeRevocationLog(t *testing.T) { require.Equal(t, testRevocationLogBytes, buf.Bytes()[:bodyIndex]) } -func TestDerializeHTLCEntries(t *testing.T) { +func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) { t.Parallel() // Read the tlv stream. @@ -210,6 +237,38 @@ func TestDerializeHTLCEntries(t *testing.T) { require.Equal(t, &testHTLCEntry, htlcs[0]) } +func TestDerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Copy the testHTLCEntry. + entry := testHTLCEntry + + // Create a fake rHash. + rHashBytes := bytes.Repeat([]byte{10}, 32) + copy(entry.RHash[:], rHashBytes) + + // Construct the serialized bytes. + // + // Exclude the first 3 bytes, which are total length, RHash type and + // RHash length(0). + partialBytes := testHTLCEntryBytes[3:] + + // Write the total length and RHash tlv. + testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...) + + // Append the rest. + testBytes = append(testBytes, partialBytes...) + + // Read the tlv stream. + buf := bytes.NewBuffer(testBytes) + htlcs, err := deserializeHTLCEntries(buf) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Len(t, htlcs, 1) + require.Equal(t, &entry, htlcs[0]) +} + func TestDerializeRevocationLog(t *testing.T) { t.Parallel()