channeldb: use BigSize to encode htlc amount

This commit uses bigsize record to encode the htlc amount, which could
save us 3 more bytes if the encoded value is no greater than roughly
0.043 bitcoin. The uint test has been updated with a more realistic
values to reflect the actual gain.
This commit is contained in:
yyforyongyu
2022-04-16 07:50:04 +08:00
parent 7c5daaf7c2
commit b288284566
2 changed files with 131 additions and 16 deletions

View File

@@ -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),
)
}

View File

@@ -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()