From 669740c84e5b328485dffdb7e3040461c70c1ed5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 17:00:55 -0700 Subject: [PATCH] channeldb: add custom blobs to RevocationLog+HTLCEntry This'll be useful for custom channel types that want to store extra information that'll be useful to help handle channel revocation cases. --- channeldb/revocation_log.go | 88 ++++++++++++++++++++++++++++---- channeldb/revocation_log_test.go | 45 ++++++++++++---- lnwallet/channel.go | 6 ++- lnwallet/channel_test.go | 2 +- 4 files changed, 120 insertions(+), 21 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 3fc4163c1..b7b73a35a 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -164,22 +164,32 @@ type HTLCEntry struct { // // NOTE: this field is the memory representation of the field amtUint. Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] + + // CustomBlob is an optional blob that can be used to store information + // specific to revocation handling for a custom channel type. + CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] } // toTlvStream converts an HTLCEntry record into a tlv representation. func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { - return tlv.NewStream( + records := []tlv.Record{ h.RHash.Record(), h.RefundTimeout.Record(), h.OutputIndex.Record(), h.Incoming.Record(), h.Amt.Record(), - ) + } + + h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + return tlv.NewStream(records...) } // NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. -func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { - return &HTLCEntry{ +func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) { + h := &HTLCEntry{ RHash: tlv.NewRecordT[tlv.TlvType0]( NewSparsePayHash(htlc.RHash), ), @@ -194,6 +204,19 @@ func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), ), } + + if len(htlc.CustomRecords) != 0 { + blob, err := htlc.CustomRecords.Serialize() + if err != nil { + return nil, err + } + + h.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + } + + return h, nil } // RevocationLog stores the info needed to construct a breach retribution. Its @@ -236,13 +259,19 @@ type RevocationLog struct { // this field, it could be the case that the field is not present for // all revocation logs. TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi] + + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This information is only created + // at channel funding time, and after wards is to be considered + // immutable. + CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] } // NewRevocationLog creates a new RevocationLog from the given parameters. func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, commitHash [32]byte, ourBalance, - theirBalance fn.Option[lnwire.MilliSatoshi], - htlcs []*HTLCEntry) RevocationLog { + theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry, + customBlob fn.Option[tlv.Blob]) RevocationLog { rl := RevocationLog{ OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0]( @@ -267,6 +296,12 @@ func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, )) }) + customBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + return rl } @@ -298,6 +333,12 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), } + commit.CustomBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + if !noAmtData { rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3]( tlv.NewBigSizeT(commit.LocalBalance), @@ -320,7 +361,10 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, return ErrOutputIndexTooBig } - entry := NewHTLCEntryFromHTLC(htlc) + entry, err := NewHTLCEntryFromHTLC(htlc) + if err != nil { + return err + } rl.HTLCEntries = append(rl.HTLCEntries, entry) } @@ -373,6 +417,10 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { }, ) + rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + // Create the tlv stream. tlvStream, err := tlv.NewStream(records...) if err != nil { @@ -413,6 +461,7 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { ourBalance := rl.OurBalance.Zero() theirBalance := rl.TheirBalance.Zero() + customBlob := rl.CustomBlob.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( @@ -421,6 +470,7 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { rl.CommitTxHash.Record(), ourBalance.Record(), theirBalance.Record(), + customBlob.Record(), ) if err != nil { return rl, err @@ -440,6 +490,10 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { rl.TheirBalance = tlv.SomeRecordT(theirBalance) } + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + rl.CustomBlob = tlv.SomeRecordT(customBlob) + } + // Read the HTLC entries. rl.HTLCEntries, err = deserializeHTLCEntries(r) @@ -454,14 +508,26 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { for { var htlc HTLCEntry + customBlob := htlc.CustomBlob.Zero() + // Create the tlv stream. - tlvStream, err := htlc.toTlvStream() + records := []tlv.Record{ + htlc.RHash.Record(), + htlc.RefundTimeout.Record(), + htlc.OutputIndex.Record(), + htlc.Incoming.Record(), + htlc.Amt.Record(), + customBlob.Record(), + } + + tlvStream, err := tlv.NewStream(records...) if err != nil { return nil, err } // Read the HTLC entry. - if _, err := readTlvStream(r, tlvStream); err != nil { + parsedTypes, err := readTlvStream(r, tlvStream) + if err != nil { // We've reached the end when hitting an EOF. if err == io.ErrUnexpectedEOF { break @@ -469,6 +535,10 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { return nil, err } + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + htlc.CustomBlob = tlv.SomeRecordT(customBlob) + } + // Append the entry. htlcs = append(htlcs, &htlc) } diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index 813f70ac9..139a02d52 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -34,6 +34,17 @@ var ( 0xff, // value = 255 } + customRecords = lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType + 1: []byte("custom data"), + } + + blobBytes = []byte{ + // Corresponds to the encoded version of the above custom + // records. + 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, + } + testHTLCEntry = HTLCEntry{ RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32]( 740_000, @@ -45,10 +56,13 @@ var ( Amt: tlv.NewRecordT[tlv.TlvType4]( tlv.NewBigSizeT(btcutil.Amount(1_000_000)), ), + CustomBlob: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5](blobBytes), + ), } testHTLCEntryBytes = []byte{ - // Body length 23. - 0x16, + // Body length 41. + 0x29, // Rhash tlv. 0x0, 0x0, // RefundTimeout tlv. @@ -59,6 +73,9 @@ var ( 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, } testHTLCEntryHash = HTLCEntry{ @@ -113,17 +130,19 @@ var ( Amt: lnwire.NewMSatFromSatoshis( testHTLCEntry.Amt.Val.Int(), ), + CustomRecords: customRecords, }}, + CustomBlob: fn.Some(blobBytes), } testRevocationLogNoAmts = NewRevocationLog( 0, 1, testChannelCommit.CommitTx.TxHash(), fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](), - []*HTLCEntry{&testHTLCEntry}, + []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes), ) testRevocationLogNoAmtsBytes = []byte{ - // Body length 42. - 0x2a, + // Body length 61. + 0x3d, // OurOutputIndex tlv. 0x0, 0x2, 0x0, 0x0, // TheirOutputIndex tlv. @@ -134,16 +153,19 @@ var ( 0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6, 0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff, 0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd, + // Custom blob tlv. + 0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, } testRevocationLogWithAmts = NewRevocationLog( 0, 1, testChannelCommit.CommitTx.TxHash(), fn.Some(localBalance), fn.Some(remoteBalance), - []*HTLCEntry{&testHTLCEntry}, + []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes), ) testRevocationLogWithAmtsBytes = []byte{ - // Body length 52. - 0x34, + // Body length 71. + 0x47, // OurOutputIndex tlv. 0x0, 0x2, 0x0, 0x0, // TheirOutputIndex tlv. @@ -158,6 +180,9 @@ var ( 0x3, 0x3, 0xfd, 0x23, 0x28, // Remote Balance. 0x4, 0x3, 0xfd, 0x0b, 0xb8, + // Custom blob tlv. + 0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, } ) @@ -269,7 +294,7 @@ func TestSerializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - expectedBytes := []byte{0x36, 0x0, 0x20} + expectedBytes := []byte{0x49, 0x0, 0x20} expectedBytes = append(expectedBytes, rHashBytes...) // Append the rest. @@ -384,7 +409,7 @@ func TestDeserializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...) + testBytes := append([]byte{0x4d, 0x0, 0x20}, rHashBytes...) // Append the rest. testBytes = append(testBytes, partialBytes...) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 02b1ea459..7c0a367da 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2420,7 +2420,11 @@ func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment, continue } - entry := channeldb.NewHTLCEntryFromHTLC(htlc) + entry, err := channeldb.NewHTLCEntryFromHTLC(htlc) + if err != nil { + return nil, 0, 0, err + } + hr, err := createHtlcRetribution( chanState, keyRing, commitHash, commitmentSecret, leaseExpiry, entry, diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 3034ccd5b..1bfd4ad88 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -10031,7 +10031,7 @@ func TestCreateBreachRetribution(t *testing.T) { revokedLog := channeldb.NewRevocationLog( uint16(localIndex), uint16(remoteIndex), commitHash, fn.Some(ourAmtMsat), fn.Some(theirAmtMsat), - []*channeldb.HTLCEntry{htlc}, + []*channeldb.HTLCEntry{htlc}, fn.None[tlv.Blob](), ) // Create a log with an empty local output index.